--- title: "09-IO流体系" created: 2026-09-02 tags: - Java --- # IO流体系 > [!note] 本篇定位 > java.io 是 Java 处理"数据搬运"的骨架:文件、网络、内存之间的读写全是流。本篇以 BIO(阻塞式 IO)体系为主干讲清"字节流/字符流双树 + 装饰器模式",再带序列化与 NIO 概念。老教程级用法(FileInputStream 基础)见 [[21-Java 流(Stream)、文件(File)、IO|鱼皮 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 都是一次系统调用,性能极差;标准写法 = 节点流 + 缓冲装饰 + 自动关闭: ```java 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-异常体系|04-异常体系]] 的补充节); - 文本读写**永远显式字符集**,别让平台默认值决定行为。 字节级复制(含二进制文件)的现代写法: ```java try (InputStream in = new BufferedInputStream(new FileInputStream(src)); OutputStream out = new BufferedOutputStream(new FileOutputStream(dst))) { in.transferTo(out); // JDK 9+,内部 8KB 缓冲循环 } ``` ## 四、对象序列化:ObjectInputStream / ObjectOutputStream 对象 ↔ 字节序列的官方机制:类实现 `Serializable`(标记接口,无方法),流负责搬运: ```java // 序列化 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` 强得多的文件工具,日常文件操作**优先用它**: ```java 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`)、符号链接处理。老代码里的 `File` 用 `file.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"要区分。 ## 勾连 - try-with-resources 与资源关闭:[[04-异常体系|04-异常体系]] - 文件句柄泄漏的排查:[[01-内存泄漏|01-内存泄漏]] - 流式处理思想在集合上的镜像:[[08-Stream与Optional|08-Stream与Optional]] - 鱼皮基础版用法(Scanner/File 流入门):[[21-Java 流(Stream)、文件(File)、IO|21-Java 流(Stream)、文件(File)、IO]] --- ⬅️ [[08-Stream与Optional|08-Stream与Optional]] 🏠 [[00-Java|00-Java]] ➡️ [[01-Object通用方法|01-Object通用方法]]