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 速查

任务核心方法
读字节InputStreamread(), read(byte[])
写字节OutputStreamwrite(int), write(byte[])
读字符Readerread(), read(char[])
写字符Writerwrite(String), write(char[])
读行BufferedReaderreadLine()
写行BufferedWriternewLine()
序列化ObjectOutputStreamwriteObject(obj)
反序列化ObjectInputStreamreadObject()

四、实战代码: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 的思路,点个赞、收个藏再走呗。有问题评论区见!

更多推荐