Skip to content

Java 中的流:IO 流与 Stream API

很多人一提 Java 里的“流”,脑子里会同时冒出两套东西:

  1. InputStreamOutputStream
  2. stream()filter()map()

这也是最容易混的地方。

因为它们虽然都叫“流”,但根本不是同一条线。

一句话先分清:

  1. IO 流 解决的是:程序怎么把数据读进来、写出去
  2. Stream API 解决的是:内存里的数据怎么按一条流水线继续处理

所以 Java 里说“流”,通常至少要分成这两类来看。


1. 先把两条线分开

先看一张压缩表:

维度IO 流Stream API
主要处理什么文件、网络、控制台、字节、字符集合、数组、内存中的数据
核心目标读写数据转换、过滤、聚合数据
常见类型InputStreamOutputStreamReaderWriterStream<T>IntStreamLongStreamDoubleStream
更像什么数据传输通道数据处理流水线
常见问题编码、缓冲、关闭资源、字节和字符惰性执行、副作用、并行流、收集结果

🌟 最容易混的一点就是:

stream() 不是 InputStream 那条线;InputStream 也不是用来做集合过滤和映射的。


2. IO 流 到底在说什么

IO 流 可以直接看成:

程序和外部世界顺着一条方向读写数据的抽象。

这里的“外部世界”包括:

  1. 文件
  2. 网络连接
  3. 控制台
  4. 内存缓冲区
  5. 其他进程之间的管道

所以 IO 流 这条线真正要解决的是:

  1. 数据从哪里来
  2. 数据往哪里去
  3. 一次读多少
  4. 要不要做缓冲
  5. 字节怎么转成字符
  6. 资源什么时候该关

2.1 Java 里的 IO 流 先按这几组分

如果只想先建立整体认知,可以按下面几组来分:

  1. 按方向分
    • 输入流:读数据
    • 输出流:写数据
  2. 按单位分
    • 字节流:处理二进制
    • 字符流:处理文本
  3. 按职责分
    • 节点流:直接连数据源
    • 处理流:在外面再包一层能力

2.2 按方向分:输入流和输出流

这组最直观:

  1. InputStream / Reader
    • 把数据读进程序
  2. OutputStream / Writer
    • 把数据从程序写出去

例如:

  1. 读文件,是输入流
  2. 写文件,是输出流
  3. 从 Socket 收数据,是输入流
  4. 往 Socket 发数据,是输出流

2.3 按单位分:字节流和字符流

这一组是工程里最容易踩坑的。

2.3.1 字节流

字节流这条线的基类是:

  1. InputStream
  2. OutputStream

它更适合处理:

  1. 图片
  2. 视频
  3. 压缩包
  4. PDF
  5. 网络协议报文
  6. 任何你不想让 Java 帮你自动按字符解释的数据

🌟 只要数据本质上是二进制,就优先站在字节流这条线上想。

2.3.2 字符流

字符流这条线的基类是:

  1. Reader
  2. Writer

它更适合处理:

  1. 纯文本文件
  2. 配置文件
  3. 日志文本
  4. 需要按字符读写的内容

但字符流不是“更高级的字节流”,它只是:

在字节和字符之间,多帮你做了一层编码解码。

所以如果是图片、音频、Excel、zip 这种二进制内容,不要跑去用 Reader / Writer

2.4 按职责分:节点流和处理流

这一组如果不分清,后面看到各种流类会越来越乱。

2.4.1 节点流

节点流可以看成:

直接连到数据源或数据目的地的流。

常见例子:

  1. FileInputStream
  2. FileOutputStream
  3. FileReader
  4. FileWriter
  5. ByteArrayInputStream
  6. ByteArrayOutputStream

它们直接面对:

  1. 文件
  2. 内存数组
  3. 字符串

2.4.2 处理流

处理流可以看成:

不直接连数据源,而是包在别的流外面,给原来的流补能力。

常见能力包括:

  1. 缓冲
  2. 编码转换
  3. 打印格式化
  4. 基本类型读写
  5. 对象序列化

常见例子:

  1. BufferedInputStream
  2. BufferedOutputStream
  3. BufferedReader
  4. BufferedWriter
  5. InputStreamReader
  6. OutputStreamWriter
  7. PrintStream
  8. PrintWriter
  9. DataInputStream
  10. DataOutputStream
  11. ObjectInputStream
  12. ObjectOutputStream

🌟 很多 IO 代码看起来一层套一层,本质上就是:节点流提供连接,处理流负责补功能。


3. Java 中常见的 IO 流 有哪些

下面这张表更适合查具体类名。

类型常见类主要干什么更适合什么场景
字节输入流InputStreamFileInputStreamByteArrayInputStream读取二进制数据文件、图片、网络报文
字节输出流OutputStreamFileOutputStreamByteArrayOutputStream写出二进制数据文件导出、图片写盘、内存拼装
字符输入流ReaderFileReaderBufferedReader读取文本配置、日志、文本文件
字符输出流WriterFileWriterBufferedWriter写出文本文本生成、日志、导出文本
缓冲流BufferedInputStreamBufferedOutputStreamBufferedReaderBufferedWriter减少频繁底层读写大多数文件 IO
转换流InputStreamReaderOutputStreamWriter做字节和字符转换指定字符集读写文本
打印流PrintStreamPrintWriter更方便写文本和格式化输出控制台、日志、导出文本
数据流DataInputStreamDataOutputStream按基本类型读写简单二进制协议、自定义文件格式
对象流ObjectInputStreamObjectOutputStream序列化和反序列化对象早期对象持久化或传输场景
管道流PipedInputStreamPipedOutputStreamPipedReaderPipedWriter线程间流式传输数据特定线程协作场景

3.1 最常用的其实是哪几类

真到业务里,最常碰到的通常就是:

  1. FileInputStream / FileOutputStream
  2. BufferedInputStream / BufferedOutputStream
  3. BufferedReader / BufferedWriter
  4. InputStreamReader / OutputStreamWriter
  5. ByteArrayOutputStream
  6. PrintWriter

也就是说,不是每一种流你都要天天手写,但这些分类边界得清楚。


4. IO 流 在工程里怎么选更稳

4.1 文件是二进制,就优先字节流

例如:

  1. 图片上传
  2. Excel 导出
  3. zip 文件处理
  4. 网络报文转存

这类场景更稳的起点通常是:

  1. InputStream
  2. OutputStream
  3. 必要时再包 Buffered*

4.2 文件是文本,就优先字符流或转换流

例如:

  1. 读取 application.yml
  2. 写日志文本
  3. 导出 CSV
  4. 处理纯文本模板

这类场景更常见的组合通常是:

  1. InputStreamReader + 指定字符集
  2. BufferedReader
  3. OutputStreamWriter + 指定字符集
  4. BufferedWriter

4.3 不要偷懒省掉字符集

这是最常见的坑之一。

例如读一个 UTF-8 文件时,如果你默认走平台字符集,到了别的机器上就可能乱码。

更稳的写法一般会显式指定:

java
try (BufferedReader reader = new BufferedReader(
        new InputStreamReader(new FileInputStream("app.log"), StandardCharsets.UTF_8))) {
    String line;
    while ((line = reader.readLine()) != null) {
        System.out.println(line);
    }
}

4.4 需要频繁读写时,优先考虑缓冲

很多同学第一次写 IO,会直接一个字节一个字节去读写。

这当然能跑,但通常不够稳。

更常见的做法是包一层缓冲流,减少频繁和底层设备打交道的次数。

例如文件复制:

java
/**
 * 用缓冲字节流复制文件。
 * 这里处理的是二进制数据,所以用字节流而不是字符流。
 */
public void copyFile(Path source, Path target) throws IOException {
    try (BufferedInputStream input = new BufferedInputStream(Files.newInputStream(source));
         BufferedOutputStream output = new BufferedOutputStream(Files.newOutputStream(target))) {

        byte[] buffer = new byte[8192];
        int len;

        while ((len = input.read(buffer)) != -1) {
            output.write(buffer, 0, len);
        }

        // 确保缓冲区里的数据真正刷到底层输出目标
        output.flush();
    }
}

4.5 flush()close() 不是一回事

这点很容易被忽略。

  1. flush()
    • 把缓冲区里的数据尽快刷出去
  2. close()
    • 关闭流,同时通常也会顺带做一次刷出和资源释放

所以如果你在长连接、网络写出、日志输出这些场景里只写不刷,可能会出现:

  1. 数据明明写了,但对方还没看到
  2. 文件内容还停在缓冲区

5. Stream API 到底在说什么

Stream APIJava 8 之后很重要的一条线。

它不是 IO 那种“数据输入输出流”,而是:

把集合、数组等内存数据,按流水线方式继续处理的一套 API。

它主要解决的是:

  1. 过滤数据
  2. 映射转换
  3. 排序
  4. 分组
  5. 聚合统计
  6. 收集结果

也就是说,Stream API 更像“加工流水线”,不是“传输管道”。

5.1 一条典型 Stream API 链路

java
List<String> names = List.of("tom", "jerry", "alice", "bob");

List<String> result = names.stream()
        .filter(name -> name.length() >= 4)
        .map(String::toUpperCase)
        .sorted()
        .toList();

这条链路做的事情很清楚:

  1. 从集合拿到流
  2. 先过滤
  3. 再转换
  4. 再排序
  5. 最后收集成结果

6. Stream API 里常见的流有哪些

6.1 Stream<T>

最常见的是对象流:

Stream<T>

它适合处理:

  1. List<User>
  2. Set<Order>
  3. MapentrySet
  4. 各种对象集合

6.2 基本类型流

Java 还专门给基本类型准备了三种流:

  1. IntStream
  2. LongStream
  3. DoubleStream

它们存在的原因不复杂:

避免基本类型频繁装箱拆箱。

例如:

java
int sum = IntStream.of(10, 20, 30, 40).sum();

6.3 并行流

还有一条经常被提到的线:

parallelStream()

它会尝试把数据处理拆开并行执行。

但这不是“性能开关”,更不是“写上就更快”。

它真正适合的前提通常包括:

  1. 数据量确实足够大
  2. 每个元素的处理逻辑比较重
  3. 任务之间几乎没有共享状态
  4. 机器资源允许并行消耗

🌟 大多数业务代码里,普通 stream() 通常比 parallelStream() 更稳。


7. Stream API 里最常见的操作有哪些

7.1 中间操作

中间操作的特点是:

先定义处理步骤,但还不真正执行。

常见的有:

  1. filter
  2. map
  3. flatMap
  4. distinct
  5. sorted
  6. limit
  7. skip
  8. peek

7.2 终止操作

终止操作的特点是:

真正触发整条流水线执行。

常见的有:

  1. forEach
  2. collect
  3. toList
  4. count
  5. findFirst
  6. anyMatch
  7. allMatch
  8. reduce
  9. min
  10. max

🌟 Stream API 一个特别容易忽略的点是:没有终止操作,前面的流水线通常不会真的跑起来。

7.3 收集器

收集器这条线也很常用,尤其是:

  1. Collectors.toList()
  2. Collectors.toSet()
  3. Collectors.toMap()
  4. Collectors.groupingBy()
  5. Collectors.partitioningBy()
  6. Collectors.joining()
  7. Collectors.counting()

例如按部门分组:

java
/**
 * 按部门把员工分组。
 */
public Map<String, List<Employee>> groupByDept(List<Employee> employees) {
    return employees.stream()
            .collect(Collectors.groupingBy(Employee::getDept));
}

8. Stream API 在工程里最容易踩哪些坑

8.1 以为 Stream 会修改原集合

通常不会。

Stream API 更常见的风格是:

  1. 从原数据出发
  2. 做一系列处理
  3. 生成新结果

8.2 在流里写太多副作用逻辑

例如:

  1. 一边 map 一边改外部变量
  2. 一边 forEach 一边发请求
  3. 一边并行流一边操作共享对象

这样写很容易让代码看起来像函数式,实际却更难排障。

8.3 把所有循环都强行改成 Stream

这也是常见问题。

不是所有循环都适合写成 Stream

如果逻辑本身:

  1. 分支很多
  2. 中途就要 break
  3. 要做复杂异常处理
  4. 可读性比原来还差

那普通 for 循环反而更合适。

8.4 滥用并行流

并行流一旦碰到:

  1. 共享状态
  2. IO 操作
  3. 顺序要求很强
  4. 数据量本来就不大

结果往往不是更快,而是更难控。


9. IO 流Stream API 到底是什么关系

它们的关系其实很简单:

  1. 名字里都有 stream
  2. 但解决的问题完全不同

更直白一点:

  1. InputStream / OutputStream
    • 关心数据怎么进出程序
  2. Stream<T>
    • 关心内存数据怎么继续加工

所以这两条线不要混。

你看到:

  1. 文件读写
  2. 网络读写
  3. 编码转换
  4. 缓冲

优先想到 IO 流

你看到:

  1. 集合过滤
  2. 映射转换
  3. 分组聚合
  4. 排序统计

优先想到 Stream API


10. 和现有 Java 主线怎么接

这一篇和当前 Java 目录里几篇笔记的关系大致是:

  1. 集合详解
    • 更偏数据结构怎么选
  2. IO、NIO 与网络编程
    • 更偏程序怎么和文件、磁盘、网络打交道
  3. 并发编程、JUC 与线程安全
    • 更偏多线程、锁、线程池、并发安全

而这一篇更偏:

把 Java 里两种最容易讲混的“流”放到同一页里彻底分开。


11. 总结

如果把 Java 里的“流”压成最核心的几句话,就是:

  1. Java 里的“流”至少有两条线:IO 流Stream API
  2. IO 流 解决的是数据怎么读进来、写出去
  3. Stream API 解决的是内存数据怎么过滤、转换、聚合
  4. IO 流 里最重要的边界是:字节流 vs 字符流、节点流 vs 处理流
  5. Stream API 里最重要的边界是:中间操作 vs 终止操作、普通流 vs 并行流
  6. 真到工程里,不要因为它们都叫“流”就混成一件事

基于 VitePress 构建的个人技术笔记。