Java I/O 概述
本教程共 100 篇 · 第 85 篇 · 更新于 2026-08-05 · 约 5 分钟阅读
85. Java I/O 概述
本节目标:搞懂「流」是什么、输入输出的方向怎么分、字节流和字符流各管什么,建立起 java.io 的整体地图。
程序为什么需要 I/O
到目前为止,我们写的程序数据都在内存里。程序一停,数据全没。
想让数据活得比程序久,就得把它写到硬盘文件里。想读取别人给的配置、想收发网络消息,也都要跟外部世界打交道。
这类「程序与外部之间搬运数据」的操作,统称 I/O,即 Input/Output(输入/输出)。
Java 把这套能力放在 java.io 包里,核心概念只有一个字:流。
流到底是什么
把流想象成一根自来水管。水管本身不存水,它只负责让水一段一段流过去。
Java 的流也一样:它不保存数据,只负责让数据一个一个地经过。数据经过的顺序是固定的,先来的先走,不能跳着取。
这根管子的两头分别接着什么,Java 并不关心。可以是硬盘上的文件、可以是网络连接、可以是内存里的一个数组、甚至可以是另一个程序。
正因为不关心,我们才能用同一套 API 处理各种数据源。这是流设计最值钱的地方。
输入还是输出,站在程序的角度看
初学者最常搞混的就是方向。记住一句话:方向永远以你自己的程序为原点。
数据从外面流进程序,叫输入流(Input Stream)。比如读文件、收网络包。
数据从程序流向外面,叫输出流(Output Stream)。比如写文件、往控制台打印。
Tip判断技巧:把「读」和「写」这两个动作代进去。你在读,就是输入;你在写,就是输出。跟文件本身没关系。
字节流与字符流的分工
Java 的流分成两大阵营,这是整个 I/O 体系最重要的一条分界线。
字节流以字节(8 位)为最小单位搬运数据。它不管这堆字节是什么含义,照单全收。图片、视频、压缩包、可执行文件,只能用字节流。
字符流以字符(Java 内部用 Unicode,一个 char 占 16 位)为最小单位。它在搬运时会自动做「字节 ↔ 字符」的编码转换。读写文本时用它,省去自己处理编码的麻烦。
一句话选型:能看懂的文本用字符流,看不懂的二进制用字节流。
Warning用字节流去读中文文本,一个汉字在 UTF-8 下占 3 个字节。若按单字节逐个转成
char,中文必然乱码。这是初学者最高频的翻车点。
四大抽象基类
两个阵营 × 两个方向,正好四个抽象基类。整个 java.io 都是围着它们建起来的。
| 方向 | 字节流 | 字符流 |
|---|---|---|
| 输入 | InputStream | Reader |
| 输出 | OutputStream | Writer |
它们都是抽象类,不能直接 new。实际用的是它们的子类,比如读文件用 FileInputStream 或 FileReader。
四个基类各自定义了最核心的方法:InputStream 的 read()、OutputStream 的 write()、Reader 的 read()、Writer 的 write()。
子类再多,也逃不出这几个动作。
节点流与处理流
流还可以按「离数据源的远近」分成两类。
节点流直接连着数据源,是真正干活的那根管子。FileInputStream 直连文件,ByteArrayInputStream 直连内存数组。
处理流不直接连数据源,而是套在别的流外面,给它加功能。BufferedInputStream 加缓冲、DataInputStream 加类型读写能力。
这种「一层套一层」的写法在 Java I/O 里到处都是:
// 三层包装:文件流 → 缓冲流 → 数据流
DataInputStream in = new DataInputStream(
new BufferedInputStream(
new FileInputStream("data.bin")));
```bash
外层流关闭时会自动关掉内层流,所以只需要关最外面那一个。
## 流是同步阻塞的
`java.io` 里的流全部是同步阻塞的。
意思是:调用 `read()` 之后,当前线程会停在那里等,直到数据真的读回来才继续往下走。
```java
int n = input.read(); // 线程卡在这行,直到读到数据或到达末尾
int m = n; // 上一行没返回,这行不会执行
硬盘和网络比 CPU 慢好几个数量级。所以 I/O 操作耗时长、且时长无法预估,这是后面学多线程时的重要背景。
必须关闭,且用 try-with-resources 关
文件句柄、网络连接这些资源由操作系统统一管理,数量有限。用完不还,程序迟早会因为句柄耗尽而崩。
所有流都有 close() 方法。但直接调用有个陷阱:中途抛异常就跳过了 close()。
Java 7 起提供了 try-with-resources 语法,把流声明在 try 后的括号里,编译器会自动生成关闭代码:
import java.io.*;
public class IoIntro {
public static void main(String[] args) throws IOException {
// 先写一个文件,保证下面读的时候一定存在
try (Writer writer = new FileWriter("demo.txt")) {
writer.write("Hello, Java I/O!");
} // 离开代码块,writer 自动关闭
// 再把它读回来
try (Reader reader = new FileReader("demo.txt")) {
int c;
while ((c = reader.read()) != -1) {
System.out.print((char) c);
}
}
System.out.println();
}
}
```bash
编译运行:
```bash
javac IoIntro.java
java IoIntro
输出:
Hello, Java I/O!
```bash
> [!NOTE]
> try-with-resources 认的不是「流」,而是 `AutoCloseable` 接口。四大基类都实现了它,所以都能放进括号里。
历史兼容写法(Java 7 之前)长这样,现在只做对照,不要在新代码里用:
```java
// 历史兼容:Java 7 之前的手动关闭,啰嗦且容易漏
InputStream input = null;
try {
input = new FileInputStream("demo.txt");
// ...读取...
} finally {
if (input != null) {
input.close();
}
}
java.io 之外还有 NIO.2
java.io 是 Java 1.0 就有的老资格。Java 7 又引入了 java.nio.file 包,提供 Path 和 Files 两个更现代的工具。
两者不是替代关系:Files 适合简单的整文件读写和目录操作,流适合大文件的分段处理。后面第 91、92 章会专门讲。
本节小结
- 流是一条单向的数据通道,只管顺序传输,不保存数据。
- 方向以自己的程序为原点:读进来是输入流,写出去是输出流。
- 二进制数据用字节流,文本数据用字符流,选错会乱码。
- 四大基类:
InputStream、OutputStream、Reader、Writer。 - 处理流套在节点流外面加功能,只需关闭最外层。
- 所有流一律用 try-with-resources 管理,不要手写
finally。