装饰器模式与IO设计
Java IO 类非常多,初学者很容易觉得混乱。但从设计模式角度看,Java IO 的核心结构并不复杂:大量 IO 类都是通过装饰器模式组合出来的。
# 1. 什么是装饰器模式
装饰器模式用于在不修改原始对象的前提下,动态增强对象能力。
在 Java IO 中:
- 节点流负责连接真实资源。
- 处理流负责包装节点流并增加能力。
InputStream input = new BufferedInputStream(new FileInputStream("data.bin"));
FileInputStream 能读文件,BufferedInputStream 增加缓冲能力。两者都属于 InputStream,所以可以透明替换。
# 2. IO 中的包装结构
一个典型的 IO 组合如下:
try (DataInputStream input = new DataInputStream(
new BufferedInputStream(
new FileInputStream("data.bin")))) {
int value = input.readInt();
}
从内到外看:
FileInputStream:连接文件。BufferedInputStream:增加缓冲。DataInputStream:增加读取基本数据类型的能力。
每一层只关心自己的职责,这就是装饰器模式的价值。
# 3. 为什么不设计成一个大而全的类
如果 Java 设计一个 SuperFileInputStream,同时支持文件读取、缓冲、对象、基本类型、编码转换,会带来几个问题:
- 类会变得极其庞大。
- 很多场景只需要其中一小部分能力。
- 能力组合不灵活。
- 扩展新能力时容易影响旧逻辑。
装饰器模式把能力拆成小组件,需要什么就组合什么。
# 4. 常见装饰器
字节流中常见的处理流:
BufferedInputStream:增加缓冲。DataInputStream:读取基本数据类型。ObjectInputStream:读取对象。PushbackInputStream:支持回退读取。DigestInputStream:读取时计算摘要。
字符流中常见的处理流:
BufferedReader:增加缓冲和按行读取。LineNumberReader:增加行号能力。PushbackReader:支持字符回退。PrintWriter:增加格式化输出能力。
# 5. 组合顺序很重要
流的包装顺序会影响能力和性能。
推荐:
new DataInputStream(new BufferedInputStream(new FileInputStream("data.bin")));
不推荐:
new BufferedInputStream(new DataInputStream(new FileInputStream("data.bin")));
前者先缓冲原始文件读取,再在缓冲后的流上解析基本类型,语义更自然。后者虽然某些情况下也能工作,但不符合常见组合习惯。
字符流也是类似:
new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8));
先把字节解码为字符,再对字符读取做缓冲和按行处理。
# 6. 关闭外层流即可吗
通常关闭最外层处理流即可。处理流的 close() 会继续关闭它包装的底层流。
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(new FileInputStream("demo.txt"), StandardCharsets.UTF_8))) {
// use reader
}
这里不需要单独关闭 FileInputStream,因为 reader.close() 会沿着包装链关闭底层资源。
不过如果多个处理流共享同一个底层流,就需要特别小心,避免某个包装对象提前关闭底层资源。
# 7. 装饰器模式带来的代价
装饰器模式灵活,但也有代价:
- 类数量多,初学者学习成本高。
- 包装层级深,代码可读性下降。
- 错误组合可能导致语义不清。
- 某些处理流有自己的缓冲,叠加不当可能浪费内存。
现代 Java 的 Files 工具类在常见场景中减少了显式包装,让代码更简单。
# 专家实践清单
| 设计点 | IO 中的体现 | 工程启发 |
|---|---|---|
| 职责拆分 | 节点流连接资源,处理流增强能力 | 单个类不要承担所有职责 |
| 组合优先 | 多层包装组合能力 | 通过组合扩展行为 |
| 面向抽象 | 构造器接收父类流 | 业务代码依赖稳定抽象 |
| 透明增强 | 缓冲、转换、数据读写叠加 | 增强不改变底层资源语义 |
典型包装结构:
FileInputStream
▼
BufferedInputStream
▼
DataInputStream
▼
业务读取 int/long/String
# Tips 快问快答
Q:Java IO 为什么适合用装饰器模式解释? A:因为很多处理流都包装另一个流,在不改变底层资源的前提下增强能力。
Q:节点流相当于装饰器里的什么? A:相当于被装饰对象,负责连接真实数据源或目标。
Q:处理流相当于什么? A:相当于装饰器,负责增加缓冲、转换、对象读写等能力。
Q:为什么不用一个大而全的 IO 类? A:职责过多会导致类膨胀,组合式设计更灵活。
Q:包装顺序重要吗? A:重要。不同顺序会影响语义,例如先转换再缓冲和先缓冲再转换的边界不同。
Q:装饰器模式有什么代价? A:层次多时可读性下降,异常栈和资源关闭链路也更复杂。
Q:为什么关闭最外层流通常够了? A:处理流关闭时会向内传递关闭动作,最终关闭底层节点流。
Q:现代 Files API 是否取代了装饰器思想?
A:没有。它只是把常见组合封装成便捷方法,底层思想仍然存在。
# 8. 总结
Java IO 的设计精髓是用装饰器模式拆分能力,再通过组合满足不同场景。理解这一点后,FileInputStream、BufferedInputStream、DataInputStream 这些类就不是孤立 API,而是一组可插拔的能力组件。