Skip to content
Go back

装饰器模式——Java IO为什么是一层套一层的洋葱?

装饰器模式: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 都转发给 inin 声明成 protected,子类(BufferedInputStreamDataInputStream)才能直接访问它、在转发前后叠加自己的增强逻辑。

BufferedInputStream.read:缓冲装饰的真相

BufferedInputStreamFilterInputStream 的子类,构造时先 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++]、从内存拿数据。& 0xffbyte 转成 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:装饰器模式有什么代价? 结论先行:调用方必须显式组装链条,且嵌套层数多时调试链路变长。因为每个装饰器都包着一层目标,异常栈里一层套一层,排查时要逐层拆开;同时它只适合「同一接口的功能叠加」,跨接口的横向扩展用不上它。


Share this post on:

Previous Post
观察者模式——Spring事件驱动架构的基石
Next Post
策略模式——Spring如何用Map注入消除if-else