← 返回题目列表

Java IO 为什么是装饰器模式的经典例子?

高频 中等 第 11 / 25 题 更新于 2026/07/28
装饰器模式Java IOInputStreamBufferedInputStream

简化版

Java IO 是装饰器模式的经典例子,因为各种输入输出流都围绕统一抽象进行包装,每一层装饰器增加一种能力,比如缓冲、数据类型读取、对象序列化等。通过组合不同流对象,可以在运行时灵活叠加功能。

详细版

Java IO 中常见写法:

InputStream in = new BufferedInputStream(
        new FileInputStream("a.txt")
);

这里:

  • InputStream 是抽象组件;
  • FileInputStream 是具体组件,负责从文件读取字节;
  • FilterInputStream 是抽象装饰器;
  • BufferedInputStream 是具体装饰器,增加缓冲能力。

再比如:

DataInputStream in = new DataInputStream(
        new BufferedInputStream(
                new FileInputStream("a.dat")
        )
);

调用链中:

  • FileInputStream 提供文件读取;
  • BufferedInputStream 提供缓冲;
  • DataInputStream 提供读取基本数据类型的能力。

如果不用装饰器,而是用继承表示所有组合,就可能需要 BufferedFileInputStreamDataBufferedFileInputStreamObjectBufferedFileInputStream 等大量类。

Java IO 用装饰器避免了类爆炸,让能力可以按需组合。

完整版教学

一、Java IO 的问题背景

输入流有很多维度的变化:

  • 数据来源:文件、内存、网络;
  • 读取方式:按字节、按基本类型、按对象;
  • 性能增强:缓冲;
  • 额外处理:解压、解密、统计。

如果用继承组合这些能力,类数量会迅速失控。

比如文件输入 + 缓冲 + 数据类型读取,可能要一个类;网络输入 + 缓冲 + 数据类型读取,又要一个类。能力一多,组合数量呈倍数增长。

装饰器模式把每种能力拆成独立包装层。

二、InputStream 体系中的装饰结构

InputStream 是所有字节输入流的抽象基类。

FileInputStream 是具体组件,负责真正读取文件。

FilterInputStream 是一个典型的装饰器基类,它内部持有另一个 InputStream

protected volatile InputStream in;

它的方法通常会委托给内部流:

public int read() throws IOException {
    return in.read();
}

BufferedInputStreamDataInputStream 等类在这个基础上增加自己的能力。

这就是标准装饰器结构:装饰器和被装饰对象同属一个抽象类型,装饰器内部持有被装饰对象。

三、BufferedInputStream 增强了什么

FileInputStream 每次读取可能涉及系统调用,频繁小读取成本较高。

BufferedInputStream 增加一个内存缓冲区。程序读取时,先从缓冲区拿数据;缓冲区不够时,再批量从底层流读取。

调用方仍然使用 InputStream

InputStream in = new BufferedInputStream(new FileInputStream("a.txt"));

这体现了装饰器的透明性:加了缓冲能力,但接口没有变。

四、DataInputStream 增强了什么

InputStream 基础能力是读字节。

DataInputStream 在字节读取基础上提供更高级的方法:

int value = dataInputStream.readInt();
double d = dataInputStream.readDouble();

它把多个字节组合解释成 Java 基本类型。

这类增强和 BufferedInputStream 不同:缓冲增强性能,DataInputStream 增强读取语义。但它们都可以作为装饰层叠加。

五、装饰顺序为什么重要

在 IO 中,装饰顺序会影响语义和性能。

new DataInputStream(new BufferedInputStream(new FileInputStream(file)))

通常比直接:

new BufferedInputStream(new DataInputStream(new FileInputStream(file)))

更符合直觉:先让底层字节读取具备缓冲能力,再在缓冲字节流上读取数据类型。

更复杂的场景如压缩、加密、编码转换,顺序更关键。先压缩再加密,和先加密再压缩,效果可能完全不同。

六、Java IO 例子给我们的启发

Java IO 展示了装饰器模式的工程价值:

  • 基础流负责数据来源;
  • 装饰流负责附加能力;
  • 每层只做一件事;
  • 组合代替继承;
  • 使用统一抽象类型连接所有层。

这就是为什么面试中一问装饰器模式,经常会追问 Java IO。

七、IO 流如何逐层增加能力

逐字节读取 8192 字节可能触发 8192 次底层读取;使用 8 KB 缓冲区时,理想情况下可降为约 1 次块读取。

FileInputStream -> BufferedInputStream -> DataInputStream -> client

这个推演把模式带来的收益和成本放在同一条调用链上。设计评审时既要确认结果正确,也要确认额外层次没有改变原有契约。

八、边界、代价与验证

检查维度应确认的内容
正确性字节流是基础能力,缓冲和按类型读取是可叠加职责,这正是 Java IO 结构的价值。
适用边界关闭最外层流通常会沿链关闭底层资源,但重复包装、提前关闭和缓冲未刷出仍需谨慎处理。
测试证据覆盖正常路径、异常路径、空值或未知类型,并断言调用次数与顺序
运行成本记录额外对象、调用层数、延迟、线程或内存开销,不用模式名称代替测量

模式落地后要用单元测试验证独立职责,用集成测试验证对象装配和真实调用入口。涉及并发、远程或异步时,还要补充竞态、超时、重复执行与资源释放测试。

易错点:字节流是基础能力,缓冲和按类型读取是可叠加职责,这正是 Java IO 结构的价值。

九、常见误区与追问

  • 误区:BufferedInputStream 改变了底层文件内容。 它改变的是访问方式和调用次数,不改变数据语义。
  • 误区:装饰器可以任意改变组件接口。 透明装饰依赖共同的 Component 契约;随意增加只能向下转型调用的方法,会破坏可替换性和继续组合的能力。
  • 误区:示例代码能运行,就代表模式边界设计正确。 能运行只证明一条路径,仍要检查异常、并发、顺序、生命周期以及新增实现时是否修改稳定代码。
  • 追问:为什么通常只关闭最外层流? 装饰流的 close 会委托下层;try-with-resources 还能保证异常时按规则释放。
  • 追问:装饰器链越长,扩展性就一定越好吗? 不一定;链越长,顺序、异常传播和排障成本越高,应由装配层集中管理并限制职责粒度。
  • 追问:怎样验证这套设计没有改变原有语义? 先做基线行为测试,再对新增层验证返回值、异常类型、调用次数和顺序;性能敏感处还要比较改造前后的量化指标。

十、加强记忆

Java IO 的装饰器思想是:文件流、网络流负责“从哪读”,缓冲流、数据流、对象流负责“怎么增强读取”。每个能力单独一层,需要什么就包什么,避免为所有组合写子类。