装饰器模式:Java IO 的洋葱模型
一句话结论(30s)
装饰器模式的本质是用组合替代继承来横向叠加功能,因为每个装饰器都持有被装饰对象引用并实现相同接口,N 个基础类 + M 个增强类就能覆盖 N×M 种组合、避免「每种组合一个类」的类爆炸。权衡:调用方需显式 new Decorator(target) 且嵌套层数多时调试链路变长,但换来功能可插拔组合——Java IO 是最经典的工业级实现。
核心原理(2min)
主流程:基础类负责核心能力(FileInputStream 读字节),装饰器类在转发调用前/后叠加增强(DataInputStream 解析格式、BufferedInputStream 加缓冲),调用方按需任意嵌套。关键机制是装饰器与目标实现同一接口、内部持有目标引用并把调用转发给它,因此能无限多层叠加;Java IO 的洋葱模型 new BufferedInputStream(new DataInputStream(new FileInputStream(...))) 正是 N+M 个类覆盖 N×M 组合的落地。与代理模式结构相同但意图相反:装饰器是调用方知情地增强功能,代理是调用方不知情地控制访问(Spring AOP 是代理)。
思考穿插:装饰器和代理结构几乎一样,怎么一句话区分? 看「调用方知不知情」+「目的是增强还是控制」:装饰器是调用方自己 new Decorator(target) 显式叠加功能,知情且为了加能力;代理是调用方拿到的就是代理对象、根本不知道有代理,目的是控制访问(权限、懒加载、远程)。所以 Java IO 是装饰器,Spring AOP 是代理——AOP 的目标类使用者全程无感。
底层深入(5-10min)
一层套一层的魔法
InputStream is = new BufferedInputStream(
new DataInputStream(
new FileInputStream("data.bin")));
// FileInputStream: 从文件读字节
// DataInputStream: 读 int/double/String(解析格式)
// BufferedInputStream: 加缓冲区(减少系统调用)
三层装饰器嵌套。顺序可任意组合——换个顺序就换了功能组合。
FilterInputStream:装饰器基类(持有被装饰的流)
装饰器「持有被装饰对象引用」的核心,落在 FilterInputStream 的字段和构造上(真实源码 jdk/src/java.base/share/classes/java/io/FilterInputStream.java):
public class FilterInputStream extends InputStream {
/**
* The input stream to be filtered.
*/
protected volatile InputStream in;
/**
* Creates a {@code FilterInputStream}
* by assigning the argument {@code in}
* to the field {@code this.in} so as
* to remember it for later use.
*/
protected FilterInputStream(InputStream in) {
this.in = in;
}
@Override
public int read() throws IOException {
return in.read();
}
}
FilterInputStream 自己也 extends InputStream——它和被包装的流实现同一个接口,这是装饰器「同构」的前提。protected volatile InputStream in 是它持有的被装饰对象,构造时把目标流记下来,之后所有 read/skip/close 都转发给 in。in 声明成 protected,子类(BufferedInputStream、DataInputStream)才能直接访问它、在转发前后叠加自己的增强逻辑。
BufferedInputStream.read:缓冲装饰的真相
BufferedInputStream 是 FilterInputStream 的子类,构造时先 super(in) 把目标流交给基类持有,再准备自己的缓冲区(真实源码 jdk/src/java.base/share/classes/java/io/BufferedInputStream.java):
public BufferedInputStream(InputStream in) {
this(in, DEFAULT_BUFFER_SIZE);
}
public BufferedInputStream(InputStream in, int size) {
super(in);
if (size <= 0) {
throw new IllegalArgumentException("Buffer size <= 0");
}
initialSize = size;
if (getClass() == BufferedInputStream.class) {
// lazily create buffer when not subclassed
buf = EMPTY;
} else {
buf = new byte[size];
}
}
super(in) 就是装饰器链条的接点——BufferedInputStream 不管 in 字段,它只维护自己的 buf(缓冲区)和 pos/count(缓冲区游标)。真正「加缓冲」的增强逻辑在单字节 read() 里:
public synchronized int read() throws IOException {
if (pos >= count) {
fill();
if (pos >= count)
return -1;
}
return getBufIfOpen()[pos++] & 0xff;
}
private void fill() throws IOException {
byte[] buffer = getBufIfOpen();
if (markpos == -1)
pos = 0; /* no mark: throw away the buffer */
else if (pos >= buffer.length) { /* no room left in buffer */
if (markpos > 0) { /* can throw away early part of the buffer */
int sz = pos - markpos;
System.arraycopy(buffer, markpos, buffer, 0, sz);
pos = sz;
markpos = 0;
} else if (buffer.length >= marklimit) {
markpos = -1; /* buffer got too big, invalidate mark */
pos = 0; /* drop buffer contents */
} else { /* grow buffer */
int nsz = ArraysSupport.newLength(pos,
1, /* minimum growth */
pos /* preferred growth */);
if (nsz > marklimit)
nsz = marklimit;
byte[] nbuf = new byte[nsz];
System.arraycopy(buffer, 0, nbuf, 0, pos);
if (!U.compareAndSetReference(this, BUF_OFFSET, buffer, nbuf)) {
throw new IOException("Stream closed");
}
buffer = nbuf;
}
}
count = pos;
int n = getInIfOpen().read(buffer, pos, buffer.length - pos);
if (n > 0)
count = n + pos;
}
read() 的核心逻辑是:pos >= count 说明缓冲区读空了,先 fill() 一把;fill() 最后一行 getInIfOpen().read(buffer, pos, buffer.length - pos) 是「一次从底层流读一大块」的关键——把逐字节的系统调用变成「一次读 8192 字节进 buf」,之后 read() 直接 return buf[pos++]、从内存拿数据。& 0xff 把 byte 转成 0~255 的无符号 int,保证返回的 -1 只用于表示 EOF、不会和合法字节值冲突。这就是装饰器「转发前后叠加增强」的落地:底层 FileInputStream 只负责读,缓冲这件「额外的事」完全由 BufferedInputStream 这一层透明叠加。
不用装饰器:类爆炸
如果不用装饰器,每种组合都要一个类:
FileInputStream
BufferedFileInputStream
DataFileInputStream
BufferedDataFileInputStream
...(N个基础 × M种增强 = N×M 组合 = 类爆炸)
装饰器让每个功能独立为一个类,用组合(传入被装饰对象的引用)替代继承——N+M 个类覆盖 N×M 种组合。
思考穿插:为什么组合能替代继承,好在哪? 继承是「编译期把能力焊死成一条继承链」,N 种基础 × M 种增强的每种组合都要一个子类,类爆炸;组合是「运行时把对象引用传进去」,每个功能独立成类,调用方按需嵌套,N+M 个类就能拼出 N×M 种组合。代价就是调用方要自己 new 出这条链,嵌套层数多了调试时栈里一层套一层——这是用「灵活」换来的「显式」。
装饰器 vs 代理模式
结构上几乎一样:都持有目标对象引用 + 实现相同接口。但意图完全不同:
| 装饰器 | 代理 | |
|---|---|---|
| 目的 | 增强功能(叠加能力) | 控制访问(权限/懒加载/远程代理) |
| 调用方 | 知情(显式 new Decorator(target)) | 不知情(Spring @Autowired 拿到代理) |
| 叠加 | 无限多层 | 通常一层 |
| 实例化 | 调用方创建目标再传给装饰器 | 代理内部持有目标,调用方不知道 |
Java IO 是装饰器:调用方显式包装。Spring AOP 是代理:@Autowired 拿到的就是代理,不知道有代理。
总结
装饰器的核心价值:组合替代继承。 把”横向扩展功能”从硬编码的继承树变成可插拔的嵌套组合。Java IO 是装饰器模式最经典的工业级实现。
章末提问
Q1:装饰器和代理模式的区别?
结论先行:结构相同但意图相反——装饰器是「知情地增强功能」,代理是「不知情地控制访问」。因为装饰器由调用方显式 new Decorator(target) 层层叠加能力,而代理在调用方无感知的情况下接管目标(Spring AOP 的 @Autowired 拿到的就是代理)。所以 Java IO 是装饰器,Spring AOP 是代理。
Q2:装饰器为什么能无限多层叠加? 结论先行:因为装饰器与目标实现同一个接口,且内部持有目标引用并转发调用。同构保证每一层都能「假装自己就是那个目标」,转发保证增强逻辑能在调用前后插入,于是可以一层套一层,顺序换一下就换一种组合。
Q3:为什么说装饰器用「组合替代继承」解决了类爆炸? 结论先行:因为组合把「能力叠加」从编译期的继承树搬成了运行时的引用嵌套。继承要为 N×M 种组合各写一个子类;组合让每个功能独立成一个类,N+M 个类即可拼出 N×M 种组合。代价是调用方要显式 new 出链条,嵌套层数多时调试链路变长。
Q4:BufferedInputStream 为什么能「一次读一大块」?
结论先行:因为它在 fill() 里一次性从底层流 read(buffer, ...) 读满缓冲区,之后的单字节 read() 直接从内存取。缓冲这件「额外的事」被这一层透明叠加,底层 FileInputStream 完全不知道也不关心,这正是装饰器「转发前后叠加增强」的落地。
Q5:装饰器模式有什么代价? 结论先行:调用方必须显式组装链条,且嵌套层数多时调试链路变长。因为每个装饰器都包着一层目标,异常栈里一层套一层,排查时要逐层拆开;同时它只适合「同一接口的功能叠加」,跨接口的横向扩展用不上它。