IO流体系
理解 Java IO,第一步不是记住所有类名,而是建立“流体系”的地图。Java IO 的类非常多,但大部分都围绕四个基础抽象展开:InputStream、OutputStream、Reader、Writer。
# 1. 什么是流
流可以理解为数据传输的管道。数据从一端流向另一端,程序只需要按照顺序读取或写入。
读取文件:文件 -> 输入流 -> Java 程序
写入文件:Java 程序 -> 输出流 -> 文件
流的好处是屏蔽底层数据源差异。无论数据来自文件、内存、网络还是压缩包,只要抽象成流,程序就可以用相似的方式处理。
# 2. 四大基础抽象
Java IO 的四个基础抽象类如下:
| 类 | 数据方向 | 数据单位 | 说明 |
|---|---|---|---|
InputStream | 输入 | 字节 | 所有字节输入流的父类 |
OutputStream | 输出 | 字节 | 所有字节输出流的父类 |
Reader | 输入 | 字符 | 所有字符输入流的父类 |
Writer | 输出 | 字符 | 所有字符输出流的父类 |
这四个类共同回答两个问题:
- 数据是读进来还是写出去?
- 数据按字节处理还是按字符处理?
# 3. 字节流体系
字节流以 byte 为最小处理单位,适合所有二进制数据。
常见字节输入流:
FileInputStream:从文件读取字节。BufferedInputStream:为字节输入增加缓冲。ByteArrayInputStream:从内存字节数组读取。DataInputStream:读取 Java 基本数据类型。ObjectInputStream:读取序列化对象。
常见字节输出流:
FileOutputStream:向文件写入字节。BufferedOutputStream:为字节输出增加缓冲。ByteArrayOutputStream:向内存字节数组写入。DataOutputStream:写入 Java 基本数据类型。ObjectOutputStream:写入序列化对象。
# 4. 字符流体系
字符流以 char 或字符数据为处理单位,适合文本内容。
常见字符输入流:
FileReader:从文件读取字符。BufferedReader:增加缓冲,支持按行读取。InputStreamReader:把字节输入流转换为字符输入流。StringReader:从字符串读取字符。
常见字符输出流:
FileWriter:向文件写入字符。BufferedWriter:增加缓冲,支持写入换行。OutputStreamWriter:把字节输出流转换为字符输出流。StringWriter:向字符串缓冲区写入字符。
# 5. 节点流和处理流
从设计上看,IO 流还可以分为节点流和处理流。
# 5.1 节点流
节点流直接连接数据源或数据目标。
例如:
FileInputStream直接读取文件。FileOutputStream直接写文件。ByteArrayInputStream直接读取内存数组。SocketInputStream直接读取网络连接。
节点流负责“接触真实资源”。
# 5.2 处理流
处理流包装在其他流外层,对原始能力进行增强。
例如:
BufferedInputStream提供缓冲能力。DataInputStream提供基本类型读写能力。ObjectInputStream提供对象反序列化能力。InputStreamReader提供字节到字符的转换能力。
处理流负责“增强已有流”。
try (InputStream input = new BufferedInputStream(new FileInputStream("data.bin"))) {
byte[] buffer = new byte[1024];
int len = input.read(buffer);
}
这里 FileInputStream 是节点流,BufferedInputStream 是处理流。
# 6. 选择流的基本原则
实际开发中可以按照下面的问题选择:
处理的是文本还是二进制?
- 文本:优先使用字符流。
- 二进制:必须使用字节流。
是否需要指定字符集?
- 需要:使用
InputStreamReader、OutputStreamWriter或Files.newBufferedReader。 - 不需要但希望代码明确:也建议显式指定
StandardCharsets.UTF_8。
- 需要:使用
文件是否较大?
- 较大:使用缓冲流或 NIO。
- 很大:避免一次性读入内存。
是否需要随机读写?
- 需要:考虑
RandomAccessFile或FileChannel。
- 需要:考虑
是否是高并发网络场景?
- 是:考虑 NIO、Netty 等基于多路复用的方案。
# 7. 常见误区
- 不要用字符流处理图片、视频、压缩包等二进制文件。
- 不要在循环中频繁单字节读取大文件,应该使用缓冲区。
- 不要依赖平台默认字符集,跨环境部署时容易乱码。
- 不要忘记关闭流,文件描述符泄漏会导致线上故障。
- 不要把大文件一次性读入
byte[]或String。
# 专家实践清单
| 检查项 | 判断标准 | 风险信号 |
|---|---|---|
| 数据单位 | 文本用字符流,二进制用字节流 | 用 Reader 处理图片、压缩包 |
| 资源所有权 | 谁创建谁关闭,外部传入要写清契约 | 工具方法关闭调用方传入的流 |
| 缓冲策略 | 高频小 IO 使用缓冲 | 循环单字节读写 |
| 字符集 | 文本读写显式指定编码 | 使用平台默认编码 |
| 大文件 | 分块或流式处理 | readAllBytes 读取超大文件 |
问题定位时可以按下面的链路倒推:
数据源
│ 字节/字符
▼
节点流
│ 文件、网络、内存
▼
处理流
│ 缓冲、转换、对象、数据
▼
业务代码
│ 解析、校验、持久化
▼
资源关闭与异常处理
# Tips 快问快答
Q:Java IO 流体系最核心的四个抽象是什么?
A:InputStream、OutputStream、Reader、Writer,分别描述方向和数据单位。
Q:节点流和处理流有什么区别? A:节点流直接连接数据源,处理流包装其他流并增强能力。
Q:为什么 IO 类这么多? A:Java IO 通过装饰器模式拆分能力,不同类负责不同维度的增强。
Q:什么时候用字节流? A:处理二进制、网络协议、压缩包、图片、视频等原始字节数据。
Q:什么时候用字符流? A:处理文本,并且需要按字符集编码或解码时。
Q:为什么要用缓冲流? A:减少底层系统调用次数,提高频繁小块读写的效率。
Q:流关闭为什么重要? A:流背后可能占用文件描述符、Socket、堆外内存等外部资源。
Q:IO 选型第一步是什么? A:先判断数据是文本还是二进制,再判断来源、大小、访问方式和可靠性要求。
# 8. 总结
Java IO 流体系看起来庞大,但主干非常清晰:四个基础抽象负责方向和数据单位,节点流负责连接真实资源,处理流负责增强能力。掌握这条主线后,再学习具体类就会容易很多。