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 I/O 的思路,点个赞、收个藏再走呗。有问题评论区见!

更多推荐