IO流体系

ℹ️本篇定位

java.io 是 Java 处理"数据搬运"的骨架:文件、网络、内存之间的读写全是流。本篇以 BIO(阻塞式 IO)体系为主干讲清"字节流/字符流双树 + 装饰器模式",再带序列化与 NIO 概念。老教程级用法(FileInputStream 基础)见 鱼皮 21-流、文件与 IO,本篇补齐体系图与进阶姿势。

一、问题提出:为什么 IO 有这么多类

读写文件、读网络、读内存块、按行读文本、解压缩……如果每种场景一个类,类的数量会爆炸。java.io 的答案是两个维度正交组合:

  • 按数据单位:字节流(8 bit,一切皆字节)与字符流(16 bit char,专为文本,多了一层"字节→字符"的编码/解码);
  • 按角色:节点流(直接连数据源:文件、数组、管道)与处理流(包在别的流外面加功能:缓冲、转换、对象化)。

第二个维度就是装饰器模式的经典落地——new BufferedInputStream(new FileInputStream(f)) 一层包一层,功能随意叠加,类数量却是"基础类 × 装饰类"的组合而不是乘积。

二、体系总览:四棵树记住全部类名

字节输入  InputStream ─── FileInputStream / ByteArrayInputStream / ObjectInputStream
              │              (节点流:文件 / 内存 / 对象反序列化)
              ├─ FilterInputStream ── BufferedInputStream / DataInputStream
              └─ InputStreamReader ─┐(字节→字符的桥)
                                    │
字符输入  Reader ─────────── FileReader ─┴─ BufferedReader / LineNumberReader

字节输出  OutputStream ─── FileOutputStream / ByteArrayOutputStream / ObjectOutputStream
              ├─ FilterOutputStream ── BufferedOutputStream / PrintStream
              └─ OutputStreamWriter ─┐
字符输出  Writer ─────────── FileWriter ─┴─ BufferedWriter / PrintWriter

记忆抓手:

  • 后缀 InputStream/OutputStream = 字节,Reader/Writer = 字符;
  • 名字带 File/ByteArray/Pipe 的是节点流,带 Buffered/Data/Object/Print/Transform 的是处理流;
  • InputStreamReader字节→字符的桥(可指定字符集),FileReader 就是它包 FileInputStream 的便捷版(用平台默认编码——这就是"读文件乱码"的头号来源,显式指定编码是铁律)。

三、正确姿势:缓冲 + try-with-resources

裸 FileInputStream 每次 read 都是一次系统调用,性能极差;标准写法 = 节点流 + 缓冲装饰 + 自动关闭:

try (BufferedReader br = new BufferedReader(
        new InputStreamReader(new FileInputStream("data.txt"), StandardCharsets.UTF_8))) {
    String line;
    while ((line = br.readLine()) != null) {   // 按行读
        System.out.println(line);
    }
}
  • 8KB 默认缓冲把系统调用摊薄一个数量级;
  • try-with-resources 自动逆序关闭(资源关闭语义见 04-异常体系 的补充节);
  • 文本读写永远显式字符集,别让平台默认值决定行为。

字节级复制(含二进制文件)的现代写法:

try (InputStream in = new BufferedInputStream(new FileInputStream(src));
     OutputStream out = new BufferedOutputStream(new FileOutputStream(dst))) {
    in.transferTo(out);   // JDK 9+,内部 8KB 缓冲循环
}

四、对象序列化:ObjectInputStream / ObjectOutputStream

对象 ↔ 字节序列的官方机制:类实现 Serializable(标记接口,无方法),流负责搬运:

// 序列化
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("u.bin"))) {
    oos.writeObject(user);
}
// 反序列化
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("u.bin"))) {
    User u = (User) ois.readObject();
}

四个必懂要点:

  1. transient 字段不序列化(密码、缓存句柄),反序列化后为默认值——transient 关键字的唯一用武之地;
  2. serialVersionUID:类结构演进(加字段)后想兼容旧数据,显式声明 private static final long serialVersionUID = 1L;;不写则编译器按类结构自动生成,类一改旧数据直接 InvalidClassException
  3. 静态字段不序列化(属于类不属于对象);可重写 writeObject/readObject 自定义规则;
  4. 安全红线:Java 反序列化漏洞(恶意构造的流触发 gadget 链)是历史重灾区——永远不要反序列化不可信来源的数据,跨服务传输用 JSON/Protobuf,Java 原生序列化只用于内部临时存储。

轻量替代:JSON(Jackson/Fastjson2)、Protobuf——现代项目里 Java 原生序列化的出场率已经很低,但面试与老系统维护仍绕不开。

五、Files / Paths:NIO.2 给文件操作换的新姿势

JDK 7 的 java.nio.file(NIO.2)提供了比 java.io.File 强得多的文件工具,日常文件操作优先用它

Path p = Path.of("data.txt");
Files.readString(p, StandardCharsets.UTF_8);            // 一次读全文
Files.writeString(p, "内容", StandardCharsets.UTF_8);   // 一次写全文
Files.readAllLines(p);                                   // 按行到 List
Files.lines(p).filter(s -> s.contains("ERROR")).forEach(System.out::println);  // 流式按行
Files.copy(src, dst, StandardCopyOption.REPLACE_EXISTING);
Files.size(p); Files.exists(p); Files.createDirectories(dir);

对比 File:错误不再返回 boolean(直接抛异常带原因)、原子操作(ATOMIC_MOVE)、目录树遍历(Files.walk)、符号链接处理。老代码里的 Filefile.toPath() 一键迁移。

六、NIO 概念:三件套与 BIO 的区别(面试层)

java.nio(New IO,JDK 4)与 BIO 的本质区别在阻塞模型

维度 BIO(java.io NIO(java.nio)
模型 一连接一线程,read 时线程干等 多路复用:一个线程用 Selector 监听多个 Channel 的就绪事件
数据单位 流(单向顺序) Channel(双向)+ Buffer(读写都经缓冲区,flip 切换)
适用 连接少、逻辑简单 连接多、长连接(如 Netty 的底层就是 NIO)

三件套:Channel(数据的通道,双向)、Buffer(数据容器,position/limit/capacity 三指针,flip() 把写模式切读模式)、Selector(事件注册中心,select() 返回就绪集合)。

AIO(NIO.2 的异步,AsynchronousSocketChannel):内核完成后回调,真异步;Linux 实现成熟度一般,实际生产主流仍是 NIO 多路复用(Netty)。

认知定位:应用层日常读写用 java.io + NIO.2 的 Files 足够;NIO/AIO 是框架层知识(Netty、Kafka、Redis 客户端的底层),会讲清模型即可,手撸 Selector 不现实。

七、高频面试题速答

  1. 字节流和字符流的区别与选择? 字节流处理一切原始数据(图片/视频/网络包);字符流处理文本,多一层按字符集的编解码。二进制内容用字节流,文本优先字符流并显式编码。
  2. BufferedInputStream 为什么快? 内存缓冲攒够 8KB 一次系统调用,把逐字节 read 的系统调用次数降低几个数量级。
  3. 序列化中 transient 和 serialVersionUID 的作用? 见第四节 1、2 点。
  4. BIO/NIO/AIO 区别一句话? BIO 等着读(阻塞)、NIO 事件通知(多路复用)、AIO 做完叫我(异步回调)。
  5. read() 返回 -1 是什么意思? 流末尾(EOF);网络流里对端关闭连接同样表现为 -1,与"暂时没数据返回 0"要区分。

勾连


⬅️ 08-Stream与Optional 🏠 00-Java ➡️ 01-Object通用方法