Appearance
Java 中的流:IO 流与 Stream API
很多人一提 Java 里的“流”,脑子里会同时冒出两套东西:
InputStream、OutputStreamstream()、filter()、map()
这也是最容易混的地方。
因为它们虽然都叫“流”,但根本不是同一条线。
一句话先分清:
IO 流解决的是:程序怎么把数据读进来、写出去Stream API解决的是:内存里的数据怎么按一条流水线继续处理
所以 Java 里说“流”,通常至少要分成这两类来看。
1. 先把两条线分开
先看一张压缩表:
| 维度 | IO 流 | Stream API |
|---|---|---|
| 主要处理什么 | 文件、网络、控制台、字节、字符 | 集合、数组、内存中的数据 |
| 核心目标 | 读写数据 | 转换、过滤、聚合数据 |
| 常见类型 | InputStream、OutputStream、Reader、Writer | Stream<T>、IntStream、LongStream、DoubleStream |
| 更像什么 | 数据传输通道 | 数据处理流水线 |
| 常见问题 | 编码、缓冲、关闭资源、字节和字符 | 惰性执行、副作用、并行流、收集结果 |
🌟 最容易混的一点就是:
stream() 不是 InputStream 那条线;InputStream 也不是用来做集合过滤和映射的。
2. IO 流 到底在说什么
IO 流 可以直接看成:
程序和外部世界顺着一条方向读写数据的抽象。
这里的“外部世界”包括:
- 文件
- 网络连接
- 控制台
- 内存缓冲区
- 其他进程之间的管道
所以 IO 流 这条线真正要解决的是:
- 数据从哪里来
- 数据往哪里去
- 一次读多少
- 要不要做缓冲
- 字节怎么转成字符
- 资源什么时候该关
2.1 Java 里的 IO 流 先按这几组分
如果只想先建立整体认知,可以按下面几组来分:
- 按方向分
- 输入流:读数据
- 输出流:写数据
- 按单位分
- 字节流:处理二进制
- 字符流:处理文本
- 按职责分
- 节点流:直接连数据源
- 处理流:在外面再包一层能力
2.2 按方向分:输入流和输出流
这组最直观:
InputStream/Reader- 把数据读进程序
OutputStream/Writer- 把数据从程序写出去
例如:
- 读文件,是输入流
- 写文件,是输出流
- 从 Socket 收数据,是输入流
- 往 Socket 发数据,是输出流
2.3 按单位分:字节流和字符流
这一组是工程里最容易踩坑的。
2.3.1 字节流
字节流这条线的基类是:
InputStreamOutputStream
它更适合处理:
- 图片
- 视频
- 压缩包
- 网络协议报文
- 任何你不想让 Java 帮你自动按字符解释的数据
🌟 只要数据本质上是二进制,就优先站在字节流这条线上想。
2.3.2 字符流
字符流这条线的基类是:
ReaderWriter
它更适合处理:
- 纯文本文件
- 配置文件
- 日志文本
- 需要按字符读写的内容
但字符流不是“更高级的字节流”,它只是:
在字节和字符之间,多帮你做了一层编码解码。
所以如果是图片、音频、Excel、zip 这种二进制内容,不要跑去用 Reader / Writer。
2.4 按职责分:节点流和处理流
这一组如果不分清,后面看到各种流类会越来越乱。
2.4.1 节点流
节点流可以看成:
直接连到数据源或数据目的地的流。
常见例子:
FileInputStreamFileOutputStreamFileReaderFileWriterByteArrayInputStreamByteArrayOutputStream
它们直接面对:
- 文件
- 内存数组
- 字符串
2.4.2 处理流
处理流可以看成:
不直接连数据源,而是包在别的流外面,给原来的流补能力。
常见能力包括:
- 缓冲
- 编码转换
- 打印格式化
- 基本类型读写
- 对象序列化
常见例子:
BufferedInputStreamBufferedOutputStreamBufferedReaderBufferedWriterInputStreamReaderOutputStreamWriterPrintStreamPrintWriterDataInputStreamDataOutputStreamObjectInputStreamObjectOutputStream
🌟 很多 IO 代码看起来一层套一层,本质上就是:节点流提供连接,处理流负责补功能。
3. Java 中常见的 IO 流 有哪些
下面这张表更适合查具体类名。
| 类型 | 常见类 | 主要干什么 | 更适合什么场景 |
|---|---|---|---|
| 字节输入流 | InputStream、FileInputStream、ByteArrayInputStream | 读取二进制数据 | 文件、图片、网络报文 |
| 字节输出流 | OutputStream、FileOutputStream、ByteArrayOutputStream | 写出二进制数据 | 文件导出、图片写盘、内存拼装 |
| 字符输入流 | Reader、FileReader、BufferedReader | 读取文本 | 配置、日志、文本文件 |
| 字符输出流 | Writer、FileWriter、BufferedWriter | 写出文本 | 文本生成、日志、导出文本 |
| 缓冲流 | BufferedInputStream、BufferedOutputStream、BufferedReader、BufferedWriter | 减少频繁底层读写 | 大多数文件 IO |
| 转换流 | InputStreamReader、OutputStreamWriter | 做字节和字符转换 | 指定字符集读写文本 |
| 打印流 | PrintStream、PrintWriter | 更方便写文本和格式化输出 | 控制台、日志、导出文本 |
| 数据流 | DataInputStream、DataOutputStream | 按基本类型读写 | 简单二进制协议、自定义文件格式 |
| 对象流 | ObjectInputStream、ObjectOutputStream | 序列化和反序列化对象 | 早期对象持久化或传输场景 |
| 管道流 | PipedInputStream、PipedOutputStream、PipedReader、PipedWriter | 线程间流式传输数据 | 特定线程协作场景 |
3.1 最常用的其实是哪几类
真到业务里,最常碰到的通常就是:
FileInputStream/FileOutputStreamBufferedInputStream/BufferedOutputStreamBufferedReader/BufferedWriterInputStreamReader/OutputStreamWriterByteArrayOutputStreamPrintWriter
也就是说,不是每一种流你都要天天手写,但这些分类边界得清楚。
4. IO 流 在工程里怎么选更稳
4.1 文件是二进制,就优先字节流
例如:
- 图片上传
- Excel 导出
- zip 文件处理
- 网络报文转存
这类场景更稳的起点通常是:
InputStreamOutputStream- 必要时再包
Buffered*
4.2 文件是文本,就优先字符流或转换流
例如:
- 读取
application.yml - 写日志文本
- 导出 CSV
- 处理纯文本模板
这类场景更常见的组合通常是:
InputStreamReader+ 指定字符集BufferedReaderOutputStreamWriter+ 指定字符集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() 不是一回事
这点很容易被忽略。
flush()- 把缓冲区里的数据尽快刷出去
close()- 关闭流,同时通常也会顺带做一次刷出和资源释放
所以如果你在长连接、网络写出、日志输出这些场景里只写不刷,可能会出现:
- 数据明明写了,但对方还没看到
- 文件内容还停在缓冲区
5. Stream API 到底在说什么
Stream API 是 Java 8 之后很重要的一条线。
它不是 IO 那种“数据输入输出流”,而是:
把集合、数组等内存数据,按流水线方式继续处理的一套 API。
它主要解决的是:
- 过滤数据
- 映射转换
- 排序
- 分组
- 聚合统计
- 收集结果
也就是说,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();这条链路做的事情很清楚:
- 从集合拿到流
- 先过滤
- 再转换
- 再排序
- 最后收集成结果
6. Stream API 里常见的流有哪些
6.1 Stream<T>
最常见的是对象流:
Stream<T>
它适合处理:
List<User>Set<Order>Map的entrySet- 各种对象集合
6.2 基本类型流
Java 还专门给基本类型准备了三种流:
IntStreamLongStreamDoubleStream
它们存在的原因不复杂:
避免基本类型频繁装箱拆箱。
例如:
java
int sum = IntStream.of(10, 20, 30, 40).sum();6.3 并行流
还有一条经常被提到的线:
parallelStream()
它会尝试把数据处理拆开并行执行。
但这不是“性能开关”,更不是“写上就更快”。
它真正适合的前提通常包括:
- 数据量确实足够大
- 每个元素的处理逻辑比较重
- 任务之间几乎没有共享状态
- 机器资源允许并行消耗
🌟 大多数业务代码里,普通 stream() 通常比 parallelStream() 更稳。
7. Stream API 里最常见的操作有哪些
7.1 中间操作
中间操作的特点是:
先定义处理步骤,但还不真正执行。
常见的有:
filtermapflatMapdistinctsortedlimitskippeek
7.2 终止操作
终止操作的特点是:
真正触发整条流水线执行。
常见的有:
forEachcollecttoListcountfindFirstanyMatchallMatchreduceminmax
🌟 Stream API 一个特别容易忽略的点是:没有终止操作,前面的流水线通常不会真的跑起来。
7.3 收集器
收集器这条线也很常用,尤其是:
Collectors.toList()Collectors.toSet()Collectors.toMap()Collectors.groupingBy()Collectors.partitioningBy()Collectors.joining()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 更常见的风格是:
- 从原数据出发
- 做一系列处理
- 生成新结果
8.2 在流里写太多副作用逻辑
例如:
- 一边
map一边改外部变量 - 一边
forEach一边发请求 - 一边并行流一边操作共享对象
这样写很容易让代码看起来像函数式,实际却更难排障。
8.3 把所有循环都强行改成 Stream
这也是常见问题。
不是所有循环都适合写成 Stream。
如果逻辑本身:
- 分支很多
- 中途就要
break - 要做复杂异常处理
- 可读性比原来还差
那普通 for 循环反而更合适。
8.4 滥用并行流
并行流一旦碰到:
- 共享状态
- IO 操作
- 顺序要求很强
- 数据量本来就不大
结果往往不是更快,而是更难控。
9. IO 流 和 Stream API 到底是什么关系
它们的关系其实很简单:
- 名字里都有
stream - 但解决的问题完全不同
更直白一点:
InputStream/OutputStream- 关心数据怎么进出程序
Stream<T>- 关心内存数据怎么继续加工
所以这两条线不要混。
你看到:
- 文件读写
- 网络读写
- 编码转换
- 缓冲
优先想到 IO 流。
你看到:
- 集合过滤
- 映射转换
- 分组聚合
- 排序统计
优先想到 Stream API。
10. 和现有 Java 主线怎么接
这一篇和当前 Java 目录里几篇笔记的关系大致是:
- 集合详解
- 更偏数据结构怎么选
- IO、NIO 与网络编程
- 更偏程序怎么和文件、磁盘、网络打交道
- 并发编程、JUC 与线程安全
- 更偏多线程、锁、线程池、并发安全
而这一篇更偏:
把 Java 里两种最容易讲混的“流”放到同一页里彻底分开。
11. 总结
如果把 Java 里的“流”压成最核心的几句话,就是:
- Java 里的“流”至少有两条线:
IO 流和Stream API IO 流解决的是数据怎么读进来、写出去Stream API解决的是内存数据怎么过滤、转换、聚合IO 流里最重要的边界是:字节流 vs 字符流、节点流 vs 处理流Stream API里最重要的边界是:中间操作 vs 终止操作、普通流 vs 并行流- 真到工程里,不要因为它们都叫“流”就混成一件事