别再死记硬背了!Java I/O 核心体系与实战避坑全解析
0、碎碎念以及思维导图
写在前面:Java I/O 这玩意儿,第一次学的人基本都懵——类名一个比一个长,参数一个比一个多,字符流、字节流、节点流、处理流……光概念就能把人绕晕。更别说"中文乱码"、“序列化版本不一致”、"读取慢如蜗牛"这些坑了。别慌,今天这篇博客,我用"水管"的比喻带你把 Java I/O 一次打通。

一、为什么 Java I/O 这么"劝退"?
我见过太多初学者写出的"反人类"代码:
// 常见错误 1:单字节读取,性能爆炸
FileInputStream fis = new FileInputStream("bigfile.dat");
int b;
while ((b = fis.read()) != -1) { // 100MB 文件 = 1亿次磁盘读取
// ...
}
// 常见错误 2:忘记关流,资源泄露
FileInputStream fis = new FileInputStream("data.txt");
// 业务异常抛出,fis 永远不会被 close()
// 文件句柄耗尽 → 线上故障
// 常见错误 3:中文乱码
FileReader fr = new FileReader("中文.txt");
// Windows 默认 GBK,Java 默认 UTF-8 —— 一片乱码
这些坑的本质:没搞清楚 Java I/O 的设计哲学——装饰器模式。一旦你理解了"流是可以一层层套娃的",所有问题迎刃而解。
二、核心概念:把 IO 流想象成"水管"
2.1 流是什么?—— 水管比喻
流(Stream) 就是数据从一端流向另一端的通道。
想象一下自来水系统:
- 数据源(磁盘文件、网络、内存)= 水库
- 流 = 水管
- 读 = 水从水库流到家里
- 写 = 水从家里流到水库
- 关闭流 = 关掉水龙头
Java 的 I/O 流就是这种"水管",但根据水的类型和管道规格分了很多种。
2.2 四个维度划分 I/O 流
| 维度 | 分类 | 比喻 | 核心类 |
|---|---|---|---|
| 数据单位 | 字节流 vs 字符流 | 输水(raw 水)vs 输饮料(加工过的) | InputStream vs Reader |
| 流向 | 输入流 vs 输出流 | 水进来 vs 水出去 | read vs write |
| 角色 | 节点流 vs 处理流 | 主管道 vs 净水器 | FileXxx vs BufferedXxx |
| 同步性 | BIO / NIO / AIO | 排队打水 / 自助取水 / 送水上门 | 本篇只讲 BIO |
记住这张表,你就掌握了 50% 的 I/O 知识。
2.3 字节流 vs 字符流:核心区别在哪?
// 字节流:操作 8 bit 的 byte —— 适合二进制(图片、视频、压缩包)
InputStream is = new FileInputStream("photo.jpg");
// 字符流:操作 16 bit 的 char —— 适合文本(.txt、.java、.json)
Reader reader = new FileReader("config.txt");
灵魂问题:字符流和字节流是啥关系?
字符流 = 字节流 + 字符编码(Charset)
字符流内部偷偷帮你做了编码转换。比如 FileReader 内部其实就是:
// FileReader 源码简化
public FileReader(String fileName) throws FileNotFoundException {
super(new FileInputStream(fileName)); // 包装字节流
// 但默认用的是系统编码!这就是中文乱码的根源
}
所以永远不要依赖默认编码。如果你要处理中文,必须显式指定:
// ✅ 正确姿势
Reader reader = new InputStreamReader(
new FileInputStream("data.txt"),
StandardCharsets.UTF_8 // 显式指定编码
);
2.4 节点流 vs 处理流:装饰器模式的精髓
这是 Java I/O 的灵魂设计,理解了这个就理解了一切。
┌──────────────┐
│ BufferedXxx │ ← 处理流:给节点流加功能(这里是"加大水箱")
└──────┬───────┘
│ 套娃包装
┌──────▼───────┐
│ FileXxx │ ← 节点流:直连数据源(这里是"主水管")
└──────┬───────┘
│
📄 文件
类比:你买了一个水龙头(节点流),但你想让水压更稳、加个过滤网、能显示流量——这些"增强功能"是另外买的小配件(处理流),往水龙头上一套就行。
为什么这样设计? 不需要为每种功能组合造一个新水龙头。你可以"嵌套"任意多层:
// 缓冲流 + 数据流 + 文件流 = 性能 + 读写基本类型
DataInputStream dis = new DataInputStream(
new BufferedInputStream(
new FileInputStream("data.bin")
)
);
三层套娃,却没有任何一行重复代码——这就是装饰器模式的优雅。
三、核心体系:一张图看懂 Java I/O
3.1 完整继承体系
java.io
│
┌───────────────────┼───────────────────┐
▼ ▼
InputStream/OutputStream Reader/Writer
(字节流 8-bit) (字符流 16-bit)
│ │
┌────┴────┐ ┌────┴────┐
▼ ▼ ▼ ▼
文件流 数组流 文件流 字符数组流
FileXxx ByteArrayXxx FileXxx CharArrayXxx
│
├────────────┬────────────┐
▼ ▼ ▼
缓冲流 数据流 对象流
BufferedXxx DataXxx ObjectXxx
3.2 高频 API 速查
| 任务 | 类 | 核心方法 |
|---|---|---|
| 读字节 | InputStream |
read(), read(byte[]) |
| 写字节 | OutputStream |
write(int), write(byte[]) |
| 读字符 | Reader |
read(), read(char[]) |
| 写字符 | Writer |
write(String), write(char[]) |
| 读行 | BufferedReader |
readLine() |
| 写行 | BufferedWriter |
newLine() |
| 序列化 | ObjectOutputStream |
writeObject(obj) |
| 反序列化 | ObjectInputStream |
readObject() |
四、实战代码:4 种方式实现文件复制
光说不练假把式。下面用"文件复制"这个经典场景,把前面讲的所有流都用一遍。
4.1 方式一:单字节复制(最慢,演示用)
/**
* 方式一:单字节读取
* 性能:极差!100MB 文件需要 1亿次磁盘读取
* 适用场景:仅用于教学演示,生产代码绝对不要用
*/
public static void copyByByte(String src, String dst) throws IOException {
// try-with-resources 自动关闭流(Java 7+)
try (InputStream in = new FileInputStream(src);
OutputStream out = new FileOutputStream(dst)) {
int b;
// 每次只读 1 字节,磁盘 IO 次数爆炸
while ((b = in.read()) != -1) {
out.write(b);
}
}
}
4.2 方式二:字节数组复制(性能提升 100 倍)
/**
* 方式二:字节数组读取(手动 buffer)
* 性能:中等,比单字节快 100 倍
* 关键:定义一个 byte[] 作为缓冲区
*/
public static void copyByArray(String src, String dst) throws IOException {
try (InputStream in = new FileInputStream(src);
OutputStream out = new FileOutputStream(dst)) {
byte[] buffer = new byte[8192]; // 8KB 缓冲区(经验值)
int len;
// 每次最多读 8KB,写入时也要写 len 字节(注意不是 buffer.length)
while ((len = in.read(buffer)) != -1) {
out.write(buffer, 0, len);
}
}
}
4.3 方式三:缓冲流(生产环境首选)
/**
* 方式三:BufferedInputStream(推荐)
* 性能:优秀,接近 NIO
* 关键:装饰器模式 —— BufferedInputStream 包装 FileInputStream
*/
public static void copyByBuffered(String src, String dst) throws IOException {
try (InputStream in = new BufferedInputStream(new FileInputStream(src));
OutputStream out = new BufferedOutputStream(new FileOutputStream(dst))) {
byte[] buffer = new byte[8192];
int len;
while ((len = in.read(buffer)) != -1) {
out.write(buffer, 0, len);
}
}
// BufferedInputStream 内部默认 8KB 缓冲
// 也可以自定义:new BufferedInputStream(in, 64 * 1024); // 64KB
}
4.4 方式四:NIO 零拷贝(极致性能)
/**
* 方式四:NIO FileChannel + transferTo(零拷贝)
* 性能:极致!底层用 sendfile 系统调用
* 适用场景:大文件复制、文件上传
*/
public static void copyByNIO(String src, String dst) throws IOException {
try (FileChannel inChannel = FileChannel.open(Path.of(src));
FileChannel outChannel = FileChannel.open(Path.of(dst),
StandardOpenOption.WRITE, StandardOpenOption.CREATE)) {
// 一行代码搞定,操作系统层面的零拷贝
inChannel.transferTo(0, inChannel.size(), outChannel);
}
}
4.5 性能对比
| 方式 | 100MB 文件耗时 | 性能 | 推荐场景 |
|---|---|---|---|
| 单字节 | ~30 秒 | ⚡ | ❌ 教学演示 |
| 字节数组 | ~1 秒 | ⚡⚡⚡ | 小文件 |
| BufferedInputStream | ~0.2 秒 | ⚡⚡⚡⚡ | ⭐⭐⭐⭐ 生产首选 |
| NIO transferTo | ~0.05 秒 | ⚡⚡⚡⚡⚡ | 大文件 / 高并发 |
性能差距高达 600 倍!这就是为什么几乎所有生产代码都用 BufferedInputStream。
4.6 完整测试代码
public class FileCopyBenchmark {
public static void main(String[] args) throws IOException {
// 先生成一个 100MB 的测试文件
try (OutputStream out = new BufferedOutputStream(
new FileOutputStream("test.bin"))) {
byte[] data = new byte[1024];
for (int i = 0; i < 100 * 1024; i++) { // 100MB
out.write(data);
}
}
// 跑测试
benchmark("单字节", () -> copyByByte("test.bin", "out1.bin"));
benchmark("字节数组", () -> copyByArray("test.bin", "out2.bin"));
benchmark("缓冲流", () -> copyByBuffered("test.bin", "out3.bin"));
benchmark("NIO", () -> copyByNIO("test.bin", "out4.bin"));
}
static void benchmark(String name, Runnable task) {
long start = System.currentTimeMillis();
task.run();
long cost = System.currentTimeMillis() - start;
System.out.printf("%-10s 耗时: %d ms%n", name, cost);
}
// 上面 4 个 copy 方法省略
}
五、对象序列化:把内存对象"打包"存到磁盘
5.1 什么是序列化?
┌────────┐ serialize ┌────────┐
│ User │ ──────────▶ │ bytes │ → 文件 / 网络
│ 对象 │ ◀────────── │ │
└────────┘ deserialize └────────┘
序列化:把 Java 对象转成字节流(写到文件 / 网络传输)
反序列化:把字节流还原成 Java 对象
最常见的应用:Redis 缓存、网络传输、远程调用(RMI)。
5.2 基础实战
// 1. 实现 Serializable 接口(标记接口,没有任何方法)
public class User implements Serializable {
// 强烈建议显式声明版本号!
// 不写的话编译器会自动生成,类一改版本号就变,反序列化就炸
private static final long serialVersionUID = 1L;
private String name;
private int age;
// transient 关键字:标记的字段不参与序列化
private transient String password; // 密码敏感,不能存到磁盘
private transient Logger logger; // Logger 不可序列化
public User(String name, int age, String password) {
this.name = name;
this.age = age;
this.password = password;
}
@Override
public String toString() {
return "User{name='" + name + "', age=" + age + ", password='" + password + "'}";
}
}
// 2. 序列化工具类
public class SerializationUtil {
/**
* 序列化:对象 → 文件
*/
public static void serialize(Object obj, String file) throws IOException {
// 用缓冲流包装对象流,提升性能
try (ObjectOutputStream oos = new ObjectOutputStream(
new BufferedOutputStream(new FileOutputStream(file)))) {
oos.writeObject(obj);
}
}
/**
* 反序列化:文件 → 对象
*/
@SuppressWarnings("unchecked")
public static <T> T deserialize(String file) throws IOException, ClassNotFoundException {
try (ObjectInputStream ois = new ObjectInputStream(
new BufferedInputStream(new FileInputStream(file)))) {
return (T) ois.readObject();
}
}
}
// 3. 测试
public static void main(String[] args) throws Exception {
User user = new User("Alice", 18, "secret123");
System.out.println("原始对象: " + user);
// 输出: User{name='Alice', age=18, password='secret123'}
// 序列化到文件
SerializationUtil.serialize(user, "user.dat");
// 从文件反序列化
User restored = SerializationUtil.deserialize("user.dat");
System.out.println("还原对象: " + restored);
// 输出: User{name='Alice', age=18, password='null'} ← transient 字段丢了
}
5.3 自定义序列化:密码加密
如果直接序列化密码字段,写入文件后就是明文,灾难。可以通过 writeObject / readObject 自定义加密:
public class SecureUser implements Serializable {
private static final long serialVersionUID = 1L;
private String name;
private transient String password; // 关键:标记为 transient
public SecureUser(String name, String password) {
this.name = name;
this.password = password;
}
/**
* 序列化时自动调用(方法签名固定,private + 无返回值)
* 实现:序列化 name + 加密后的 password
*/
private void writeObject(ObjectOutputStream oos) throws IOException {
oos.defaultWriteObject(); // 序列化非 transient 字段(name)
oos.writeObject(encrypt(password)); // 加密后写入密码
}
/**
* 反序列化时自动调用
* 实现:读取 name + 解密 password
*/
private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
ois.defaultReadObject();
this.password = decrypt((String) ois.readObject());
}
// 简单的 Base64 加密示例(生产环境用 AES)
private String encrypt(String s) {
return s == null ? null : "ENC[" + s + "]";
}
private String decrypt(String s) {
return s == null ? null : s.substring(4, s.length() - 1);
}
@Override
public String toString() {
return "SecureUser{name='" + name + "', password='" + password + "'}";
}
}
5.4 序列化常见坑
坑 1:父类没实现 Serializable
public class Animal {
protected int id; // 父类字段
}
public class Dog extends Animal implements Serializable {
private String name;
}
// ❌ 反序列化时 Animal 部分字段丢失(id 变成 0)
// 解决:父类也实现 Serializable,或者提供无参构造
坑 2:静态字段不序列化
public class Counter implements Serializable {
public static int count = 0; // 静态字段属于类,不属于对象
}
// 序列化只保存对象实例状态,static 字段不会保存
坑 3:单例被序列化破坏
public class Singleton implements Serializable {
private static final Singleton INSTANCE = new Singleton();
// 反序列化时会创建新对象,破坏单例!
// 解决:加 readResolve 方法
private Object readResolve() {
return INSTANCE; // 强制返回单例
}
}
六、避坑指南
6.1 资源管理
// ❌ 反面教材:手动关流,繁琐且容易忘
FileInputStream fis = null;
try {
fis = new FileInputStream("data.txt");
// ...
} catch (IOException e) {
// ...
} finally {
if (fis != null) {
fis.close();
}
}
// ✅ 最佳实践:try-with-resources
try (FileInputStream fis = new FileInputStream("data.txt");
BufferedInputStream bis = new BufferedInputStream(fis)) {
// 业务代码
}
// 流自动关闭,异常自动处理,代码量减半
6.2 字符编码
// ❌ 依赖系统默认编码,跨平台就炸
new FileReader("data.txt");
// ✅ 显式指定 UTF-8
new InputStreamReader(new FileInputStream("data.txt"), StandardCharsets.UTF_8);
6.3 性能优化
// ❌ 裸流读写,IO 次数爆炸
new FileInputStream("data.bin");
// ✅ 永远用缓冲流包装
new BufferedInputStream(new FileInputStream("data.bin"));
6.4 序列化版本
// ❌ 不写 serialVersionUID,类一改就炸
public class User implements Serializable { }
// ✅ 永远显式声明
public class User implements Serializable {
private static final long serialVersionUID = 1L;
}
6.5 敏感字段
// ❌ 敏感字段直接序列化
public class User implements Serializable {
private String password; // 灾难!
}
// ✅ 用 transient 标记 + 自定义加密
public class User implements Serializable {
private transient String password;
}
6.6 IO 选型速查表
| 需求 | 推荐方案 |
|---|---|
| 读 / 写文本文件 | BufferedReader / BufferedWriter + InputStreamReader(UTF-8) |
| 读 / 写二进制文件 | BufferedInputStream / BufferedOutputStream |
| 复制大文件 | NIO FileChannel.transferTo |
| 读写 Java 对象 | ObjectInputStream / ObjectOutputStream |
| 处理 GBK 等老编码 | InputStreamReader(fis, Charset.forName("GBK")) |
| 按行读取 | BufferedReader.readLine() |
| 格式化输出 | PrintWriter + BufferedWriter |
| 配置文件 | Properties.load(reader) |
七、总结:一张图回顾 Java I/O
Java I/O 体系
│
├── 数据单位
│ ├── 字节流 (byte 8-bit) → InputStream / OutputStream
│ └── 字符流 (char 16-bit) → Reader / Writer
│
├── 角色
│ ├── 节点流 → FileXxx(直连数据源)
│ └── 处理流 → BufferedXxx(套娃加功能)
│
├── 核心原则
│ ├── 性能:永远用缓冲流
│ ├── 资源:用 try-with-resources
│ ├── 编码:显式指定 UTF-8
│ └── 序列化:transient + writeObject 自定义
│
└── 选型口诀
文本 → 字符流 + 缓冲 + 显式编码
二进制 → 字节流 + 缓冲
大文件 → NIO transferTo
对象 → ObjectInputStream + 自定义序列化
记住这 4 个核心原则 + 选型口诀,Java I/O 你就算入门了。
参考资料
- Java Basic I/O Tutorial (Oracle)
- java.io Package Summary (Java 25)
- Java Object Serialization Specification
- Effective Java, 3rd Edition — Items 85-90 (序列化章节)
📌 码字不易,如果这篇博客帮你理清了 Java I/O 的思路,点个赞、收个藏再走呗。有问题评论区见!
更多推荐


所有评论(0)