看到 ArrayList.Itr、BufferedInputStream、PropertyChangeSupport 这类 JDK 类,很容易把它们直接贴上 Iterator、Decorator、Observer 的标签。标签本身并不是证据。真实库里的类经常同时承担兼容性、性能、序列化、线程安全和历史 API 约束;只看类名、继承关系或“持有另一个对象”,会把结构相似误读成模式意图。

本章固定到 OpenJDK jdk-21+35 对应提交 890adb6410dab4606a4f26a942aed02fb2f55387。本机运行环境是 Homebrew OpenJDK 21.0.10;它只用于验证 Java 21 标准库行为和 src.zip 路径存在,不声明本机发行包源码与上游 GA 提交逐字一致。

Iterator 要看调用者拿到什么遍历协议

OpenJDK 的 ArrayList.iterator() 在固定提交里返回内部 Itr;Itr 持有 cursor、lastRet 和 expectedModCount,next() 先检查结构性修改,再按当前位置返回元素。源码路径是 src/java.base/share/classes/java/util/ArrayList.java,入口在 iterator(),实现类在 Itr。

这和本系列第 34 篇的教学 Iterator 对得上:调用方依赖 Iterator 协议,而不是依赖内部数组和下标细节。labs/E07 先用 After.readWithIterator 遍历 paper, pen,再在拿到迭代器后修改原 ArrayList,断言后续 next() 抛 ConcurrentModificationException。这个断言只说明 fail-fast 边界存在;它不证明并发安全,也不把所有“能遍历”的 API 都叫 GoF Iterator。

1
2
3
4
Iterator<String> iterator = names.iterator();
assertEquals("paper", iterator.next());
names.add("eraser");
assertThrows(ConcurrentModificationException.class, iterator::next);

真实库中的模式识别先找调用者稳定依赖的协议。Iterator 的证据不是“类里有 cursor”,而是调用方能通过统一遍历协议访问不同存储,同时边界条件有明确行为。

Decorator 要看包装后增加了什么责任

FilterInputStream 的 Javadoc 和源码说明它保存一个被过滤的 InputStream,默认把请求转发给这个被包装对象;BufferedInputStream 继承它,并增加缓冲、mark 和 reset 支持。固定提交里的源码路径分别是 src/java.base/share/classes/java/io/FilterInputStream.java 与 src/java.base/share/classes/java/io/BufferedInputStream.java。

labs/E07 用 BufferedInputStream(new ByteArrayInputStream(...)) 验证 markSupported()、mark()、reset() 和继续读取后的结果。这里把它称作 Decorator 是有边界的:它保留 InputStream 接口,包装另一个 InputStream,并添加可观察的新责任。反例是 PassthroughInputStream extends FilterInputStream。它也有同样的继承形状,但只是转发;旧的 Before.byShapeOnly 会把它标成 Decorator,测试则用一个不支持 mark 的输入流证明它没有新增缓冲行为。

1
2
3
4
5
6
BufferedInputStream input = new BufferedInputStream(new ByteArrayInputStream(new byte[] {1, 2, 3}));
input.mark(3);
assertEquals(1, input.read());
assertEquals(2, input.read());
input.reset();
assertEquals(1, input.read());

结构相似只给出候选,不给出结论。Decorator 的关键不是“继承同一个父类”,而是在相同接口下包住原对象,并把附加职责放在包装层。

PropertyChangeSupport 更适合说成监听支持

PropertyChangeSupport 位于 src/java.desktop/share/classes/java/beans/PropertyChangeSupport.java。它维护 PropertyChangeListener 列表,提供 addPropertyChangeListener、removePropertyChangeListener 和 firePropertyChange;当旧值和新值都非空且相等时,不派发事件。Javadoc 还声明这个支持类是 thread-safe。

它和 Observer 的关系应谨慎表达。它确实让发布者不需要知道每个监听者的具体动作,也允许多个监听者响应同一个属性变化;但它的 API 名称、JavaBeans 历史、属性名过滤和序列化约束,都比 GoF 书里的 Subject/Observer 结构更具体。本文把它称作“监听者/Observer 风格的发布订阅支持”,不把它硬说成 GoF Observer 的直接实现。

labs/E07 只断言三个本地行为:draft -> paid 会送达,paid -> paid 不送达,退订后 paid -> closed 不送达。没有测试 Swing 事件线程、跨进程序列化、监听器异常策略或并发注册。

1
2
3
4
5
support.addPropertyChangeListener(listener);
support.firePropertyChange("state", "draft", "paid");
support.firePropertyChange("state", "paid", "paid");
support.removePropertyChangeListener(listener);
support.firePropertyChange("state", "paid", "closed");

真实库的命名、历史和 API 契约要优先于模式标签。能说“这个类承担发布订阅协作的一部分”,不等于可以忽略它自己的领域语义。

三个例子的调用证据

flowchart LR
    A[After.readWithIterator] --> B[ArrayList.iterator]
    B --> C[ArrayList.Itr.next]
    C --> D[fail-fast 检查 expectedModCount]

    E[After.readThroughFilter] --> F[BufferedInputStream]
    F --> G[被包装的 ByteArrayInputStream]
    F --> H[mark/reset 缓冲责任]

    I[After.propertyEvents] --> J[PropertyChangeSupport]
    J --> K[PropertyChangeListener]
    J --> L[相等值不派发]

这张图只说明三个调用链各自验证了什么:第一个验证统一遍历协议,第二个验证包装后新增缓冲责任,第三个验证监听注册和派发规则。它不把所有相似 API 都强行归入 GoF,也不把 PropertyChangeSupport 写成完整异步 Observer 框架。

三个判定问题

看真实库时,模式名应排在证据之后。一个可复查的判断至少回答三个问题:

  1. 调用者稳定依赖的接口或协议是什么。
  2. 被稳定下来的变化方向是什么。
  3. 测试能否证明这个协作边界,而不是只证明类名相似。

如果这三个问题回答不上来,用 API 自己的名字更诚实。Alternative.nameApisInsteadOfPatterns() 直接写“ArrayList.iterator 返回 fail-fast Iterator”、“BufferedInputStream 添加缓冲与 mark/reset”、“PropertyChangeSupport 管理监听注册和派发”。这比把所有例子都塞进 GoF 名称更不容易误导读者。

案例 可说的模式关系 本章证据 不能外推
ArrayList.Itr 与 Iterator 意图吻合 统一遍历协议、fail-fast 边界 并发安全
BufferedInputStream 与 Decorator 意图吻合 包装 InputStream 并添加缓冲、mark/reset 所有 FilterInputStream 子类都是有效装饰器
PropertyChangeSupport Observer 风格的监听支持 注册、派发、相等值不派发、退订 完整异步消息或 GoF 原书结构一一对应

实验、边界与练习

在 examples/design-patterns/ 执行:

1
JAVA_HOME=/opt/homebrew/opt/openjdk@21/libexec/openjdk.jdk/Contents/Home ./mvnw -B -ntp -pl labs/E07 -am test

4 项测试通过;环境、命令、源码路径检查、OpenJDK tag object 和固定 SHA 源码副本见 examples/design-patterns/evidence/E07/RUN.md 与 examples/design-patterns/evidence/E07/OPENJDK.md。本机 src.zip 只用于路径存在性和运行行为验证。

  1. 把 PassthroughInputStream 改成真正缓存第一个字节的包装器,先保留旧 passthrough 反例,再新增 mark/reset 或重复读取行为断言,判断它是否仍应叫 Decorator。
  2. 给 PropertyChangeSupport 增加一个会抛异常的监听器,先运行现有订阅/退订合同,再查 JDK 行为并写出异常传播断言;不要从“Observer”标签推断错误策略。

上一节:E06 从 DIP 到 Ports and Adapters;下一节:E08 遗留代码的小步重构;可选回到:39 从变化需求保留必要模式。

参考资料