Java 设计模式 · 观察者模式(Observer Pattern)系统学习文档
一、总览(先看这张图,后面全部围绕它展开)
【谁的变化,要通知谁】
┌─────────────────────────────────────────────────────────┐
│ Subject 被观察者 │
│ (持有状态 + 持有一组 Observer 的引用) │
│ │
│ register(observer) ← 注册:以后有事叫我 │
│ unregister(observer) ← 反注册:别再叫我了 │
│ notifyAll() ← 状态变了,挨个通知 │
└───────────────┬─────────────────────────────────────────┘
│ 状态变化时主动遍历通知
┌───────────┼───────────┬───────────────┐
▼ ▼ ▼ ▼
┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐
│ 观察者1 │ │ 观察者2 │ │ 观察者3 │ ... │ 观察者N │
│ 短信 │ │ 邮件 │ │ 站内信 │ │ 数据看板│
└────────┘ └────────┘ └────────┘ └────────┘
▲ ▲ ▲
└───────────┴───────────┘
全部依赖同一个 Observer 接口,
彼此不认识,新增/删除谁都不用改 Subject 的代码
一句话抓住本质:
一对多依赖 :当 A 的状态改变时,所有依赖 A 的对象自动收到通知并更新自己。
关键在于 ------ A 不认识具体的 B/C/D ,只认识一个
Observer接口。
二、概念介绍
2.1 定义
| 来源 | 说法 |
|---|---|
| GoF《设计模式》 | 定义对象间的一种一对多的依赖关系,当一个对象(Subject)的状态发生改变时,所有依赖于它的对象(Observer)都得到通知并自动更新。 |
| 更口语的说法 | "订阅---通知":我订阅你,你有动静就喊我一声。 |
2.2 四个核心角色
| 角色 | 职责 | 本文对应 |
|---|---|---|
| Subject(主题/被观察者) | 持有观察者列表,状态变化时遍历通知 | SimpleSubject / SafeSubject |
| Observer(观察者) | 定义收到通知后的更新动作 | interface Observer |
| ConcreteSubject | 具体的主题实现(有真实业务状态) | 订单状态变更的 Subject |
| ConcreteObserver | 具体的观察者实现 | SmsObserver、EmailObserver |
注意 :骨架里
SimpleSubject同时充当了 Subject 和 ConcreteSubject,这是教学时的简化。真实项目中如果只有一种主题,不必强行拆两层。
2.3 它属于哪一类模式
- 目的:对象行为型模式(关注对象之间怎么协作)
- 关系:解耦"事件产生方"和"事件响应方"
- 核心手法 :面向接口编程 + 组合 (用一个
List<Observer>替代硬编码调用)
三、为什么需要它(问题 → 方案 → 为什么 → 收益)
3.1 典型问题:硬编码的"扇出"调用
假设订单状态一变,要通知短信、邮件、站内信三个渠道:
❌ 反面写法(最常见)
java
public void changeStatus(Order order) {
this.status = order.status;
smsService.send(order); // 硬编码依赖 1
emailService.send(order); // 硬编码依赖 2
siteService.send(order); // 硬编码依赖 3
}
问题出在哪:
| 症状 | 后果 |
|---|---|
| 加一个渠道(如"App 推送") | 必须改订单类 ,违反开闭原则(对扩展开放、对修改关闭) |
| 渠道逻辑变了 | 又要去改订单类,职责混杂 |
| 渠道之间有先后依赖 | 通知逻辑越堆越乱,订单类越来越胖 |
| 想临时关掉邮件通知 | 只能改代码,没法运行期控制 |
根本矛盾 :订单类本该只管订单,却被逼着认识一堆通知渠道。
3.2 方案:引入 Observer 接口
✅ 观察者写法
java
public void changeStatus(Order order) {
this.status = order.status;
for (Observer o : observers) { // 只认识接口,不认识具体是谁
o.update(order);
}
}
3.3 为什么这样就对了
| 维度 | 解释 |
|---|---|
| 依赖方向反转 | 订单类不再依赖 3 个具体服务,只依赖 1 个抽象 Observer ------ 依赖倒置 |
| 满足开闭原则 | 加渠道 = 写一个新 Observer 实现 + attach(),订单类一行不改 |
| 职责单一 | 订单只负责"喊一声",怎么处理是各观察者自己的事 |
| 运行期可控 | attach/detach 让通知对象集可以动态变化,不必重启 |
3.4 收益总结
改动成本:
硬编码: 新增渠道 = 改订单类 + 回归测试订单逻辑 (耦合度 高)
观察者: 新增渠道 = 新建一个类 + 一行 attach (耦合度 低)
风险面:
硬编码: 通知逻辑出问题可能波及订单核心流程
观察者: 靠异常隔离,单个观察者出错不会拖垮订单(见 5.2)
面试表达 :观察者模式的本质是把"一对多的扇出调用"变成"面向接口的广播",用运行期的动态订阅替代编译期的硬编码依赖。
四、观察者的两种数据传递方式:推模式 vs 拉模式
这是观察者模式最重要的一个分支,面试高频。
4.1 推模式(Push)------ 我把数据塞给你
java
void update(Order order); // 被观察者直接把完整数据传过去
- 优点:观察者拿到即用,简单直接
- 缺点 :Subject 必须知道 Observer 需要什么;数据结构一变,所有 Observer 都受影响;观察者可能只关心一个字段却被迫接收全量
4.2 拉模式(Pull)------ 我只告诉你"变了",你自己来取
java
void update(Subject source); // 只给个信号/引用
// 观察者内部:
Order o = source.getLatest(); // 自己回查
- 优点 :Subject 只需提供通用取数接口,耦合更松;观察者按需取字段
- 缺点 :观察者要多一次取数,且需要能访问 Subject 的内部状态(通常暴露 getter)
4.3 怎么选
| 场景 | 建议 |
|---|---|
| 数据小而稳定、观察者都需要全量 | 推模式(省事) |
| 数据大/字段多、观察者各取所需 | 拉模式(省流量、松耦合) |
| 拿不准 | 推"精简版",或推一个"事件对象"让观察者自己判断要不要继续取 |
实测代码 Demo1 走推 ,Demo3 走拉,附录 A 可对照看输出差异。
五、代码讲解(从骨架到工程化)
以下代码全部实测可运行,完整源码见附录 B。
5.1 最小骨架(能跑通的最简形态)
java
// 观察者接口
interface Observer {
void update(Order order);
}
// 被观察者
class SimpleSubject {
private final List<Observer> observers = new CopyOnWriteArrayList<>();
private Order latest;
public void attach(Observer o) { observers.add(o); }
public void detach(Observer o) { observers.remove(o); }
public void changeStatus(Order o) {
this.latest = o;
notifyAllObservers();
}
private void notifyAllObservers() {
for (Observer o : observers) {
o.update(latest); // 推模式:直接给数据
}
}
}
讲解要点:
observers用 接口类型 装 ------ 这是解耦的关键,具体是谁无所谓attach/detach让集合运行期可变,这是相对硬编码的最大优势latest保存最新状态,notifyAllObservers()遍历广播
实测验证了三件事(附录 A Demo1):
- ✅ 新增观察者后,下一次通知能收到
- ✅ 反注册后,不再收到
- ✅ 已注册的观察者每次都收到
5.2 工程化要点一:异常隔离(⭐ 高含金量)
问题 :如果通知循环里某个观察者抛异常,for 循环会直接中断,后面的观察者全部收不到通知。
java
for (Observer o : observers) {
o.update(latest); // ← 这里抛异常,循环终止,3号4号再也收不到
}
方案 :给每个观察者单独 try-catch。
为什么 :通知是广播,性质上应是"尽力送达"------一个渠道挂了(比如邮件服务连接池耗尽),不该让订单状态通知整体失败。
收益 :故障隔离,单点故障不扩散。这也是"观察者模式在生产环境必须做的加固"。
java
for (Observer o2 : List.copyOf(observers)) {
try {
o2.update(latest);
} catch (Exception e) {
// 吞掉并记录,不让单个观察者拖垮整条通知链
System.out.println(" [隔离] 观察者 " + o2.name() + " 异常被吞:" + e.getMessage());
}
}
实测结果 (附录 A Demo2)------坏观察者抛异常,后面的人照样收到:
[短信] 138****0001 收到:Order{B2002, 退款中}
[隔离] 观察者 坏观察者 异常被吞:连接池耗尽
[邮件] li@x.com 收到:Order{B2002, 退款中} ← 没被影响
5.3 工程化要点二:线程安全与遍历(⭐)
问题 :如果一个线程在 changeStatus() 遍历通知,另一个线程同时 attach/detach(),会抛 ConcurrentModificationException。
两个方案:
| 方案 | 做法 | 适用 |
|---|---|---|
| A. 换容器 | new CopyOnWriteArrayList<>() |
写极少、读极多(通知列表正是此场景) |
| B. 遍历前拷贝 | for (Observer o : List.copyOf(observers)) |
通用兜底,不依赖容器实现 |
为什么推荐 COW:观察者列表几乎总是"注册/反注册很少、每秒通知很多",COW 写时复制的代价正好落在低频操作上,读时无锁、迭代零冲突。
实测结果(附录 A Demo4)------并发通知无异常:
并发下无 ConcurrentModificationException
诚实标注 :Demo4 是单次并发 的验证,能证明遍历不会抛 CME;不等于证明了高并发下的完整正确性(如通知顺序、可见性等)。若要做严格验证,需要压力测试------本测试未做。
5.4 ⭐ 实测翻车点:反注册失效(最容易踩的坑)
这是我写文档时真实跑出来的 bug,值得单独讲。
现象 :写了 detach(),以为反注册成功,结果观察者还在收通知。
java
subject.attach(new EmailObserver("wang@x.com"));
subject.detach(new EmailObserver("wang@x.com")); // 同样的内容,但是【新对象】
subject.changeStatus(order);
// 预期:邮件不收。实际:邮件照样收到!
根因 :List.remove(Object) 内部用 equals 比较。EmailObserver 没重写 equals,走 Object 的引用相等 ------new 出来的两个对象引用不同 → 移除失败,但也不报错,静默失效。
为什么危险 :这是静默 bug------不抛异常、不报错,只是通知停不下来,在生产环境表现为"用户已经取消订阅了,还在收到短信"。
三个解法(实测对比见附录 A Demo0):
| 解法 | 写法 | 评价 |
|---|---|---|
| A. 持有同一引用 | EmailObserver ref = ...; attach(ref); detach(ref); |
最稳,推荐 |
B. 重写 equals/hashCode |
让"同内容"视为同一观察者 | 适合"按身份订阅"的场景 |
| C. 用唯一 key 管理 | Map<String, Observer> 按 id 移除 |
工程上最可控 |
实测输出(三种写法同框对比):
[坑A] 用【新实例】反注册 + 未重写 equals → 移除失败
[邮件] wang@x.com 收到:Order{A000, 测试} ← 仍收到,detach 没生效
[坑B] 用【同一个引用】反注册 → 成功
↑ 邮件没收到 = detach 生效
[坑C] 重写 equals/hashCode 后,新实例也能移除
↑ 邮件没收到 = equals 让 detach 生效
面试价值 :能主动讲出这个坑,说明你真写过、真踩过,而不是背概念------这是"深水区"信号(见第八节分级)。
5.5 为什么不用 JDK 自带的 java.util.Observable
实测事实(JDK 21,编译即警告):
Observable 类上注解: @java.lang.Deprecated(forRemoval=false, since="9")
Observer 接口上注解: @java.lang.Deprecated(forRemoval=false, since="9")
它为什么被废弃:
| 缺陷 | 说明 |
|---|---|
| 继承而非接口 | extends Observable 占用唯一父类,无法复用 |
| "半成品"封装 | setChanged() 要手动调用才触发通知,状态管理割裂 |
| 线程不安全 | 内部用 Vector + 手工 notify,并发下易出问题 |
| 设计陈旧 | 未考虑泛型、Lambda 等现代特性 |
结论 :JDK 9 起废弃,新代码不要用。正确做法是:
- 简单场景:自己写 10 行(如本文骨架)
- Java 8+:用
Consumer/回调接口或事件总线 - Spring:用
ApplicationEventPublisher(见 6.3)
六、常见使用场景
6.1 JDK 标准库中的观察者
| 位置 | 形式 |
|---|---|
java.beans.PropertyChangeListener |
JavaBean 属性变化监听(未废弃,可用) |
Swing/AWT 事件模型 |
按钮点击等 UI 事件(ActionListener) |
java.util.Observer/Observable |
已废弃(JDK 9),见 5.5 |
InputStream 的 available/自定义流包装 |
部分流处理采用回调式设计(弱相关) |
诚实标注 :前两项是确定的标准用法;第三项已废弃。第四项仅为"回调式设计"的类比,不算严格观察者模式,面试时别硬套。
6.2 框架中的观察者
| 框架 | 机制 |
|---|---|
| Spring 事件机制 | ApplicationEventPublisher + @EventListener,最贴近观察者的生产级实现 |
| Servlet 监听器 | ServletContextListener、HttpSessionListener |
| Log4j/Logback Appender | 日志事件广播给多个 Appender |
| 响应式编程(Reactor/RxJava) | Flux 订阅模型是观察者的思想延伸 |
| MQ(消息队列) | 发布-订阅是观察者的"分布式放大版"(见第七节) |
6.3 典型业务场景
- 订单状态变更 → 多渠道通知(短信/邮件/站内信/推送)------ 本文示例
- 库存扣减 → 预警 + 看板 + 日志
- 用户注册成功 → 发券 + 赠送积分 + 首封邮件
- 配置中心变更 → 各服务动态刷新
- 审批流推进 → 通知发起人/抄送人/管理员
- 实时协作/聊天室 → 一条消息广播给多个在线客户端
- 数据看板:指标变化 → 多个图表刷新
6.4 反向判断:什么时候不该用
说清楚边界,比会用更有价值。
| 不适用的情况 | 原因 |
|---|---|
| 只有一个观察者 | 用不着抽象,直接调用更简单 |
| 观察者数量固定且永不变化 | 动态订阅的价值用不上 |
| 通知有严格的同步/事务要求 | 观察者通知通常在状态变更后触发,难与事务边界对齐(可能用"事务同步"或 MQ 更合适) |
| 观察者之间有复杂顺序依赖 | 观察者模式不定义顺序,硬塞会让设计变味 |
| 需要可靠投递、重试、削峰 | 这是 MQ 的职责,进程内观察者做不到 |
七、观察者 vs 发布-订阅(面试必考对比)
很多面试官会追问这个,务必分清。
【观察者模式】(进程内、直连)
Subject ────────────────▶ Observer
直接持有引用,直接调用
【发布-订阅】(中间多了一个"事件通道")
Publisher ──▶ Event Channel/Broker ──▶ Subscriber
(解耦了双方)
| 维度 | 观察者模式 | 发布-订阅 |
|---|---|---|
| 中间层 | 无,Subject 直接持有 Observer 引用 | 有,经由事件通道/Broker 中转 |
| 耦合度 | 双方仍相互感知(Subject 持有引用) | 双方完全不感知 |
| 范围 | 进程内 | 可跨进程、跨服务、跨机器 |
| 同步性 | 通常同步(通知时直接调用) | 通常异步 |
| 失败处理 | 需自己做异常隔离 | 由 MQ 提供重试/持久化/削峰 |
| 典型实现 | 本文代码 | Kafka、RabbitMQ、Redis Pub/Sub |
一句话记忆:
观察者是"直接喊人",发布订阅是"发到群里,谁在谁看"。
发布-订阅可以看作"观察者模式 + 中间 Broker"的演化。
面试加分说法 :MQ 的 Topic 订阅、Spring 的 @EventListener(同进程)分别落在两端------Spring 事件更接近观察者,MQ 更接近发布订阅。
八、优缺点
优点
- 松耦合:Subject 只依赖 Observer 接口,双方可独立变化
- 满足开闭原则:新增观察者无需修改 Subject
- 动态订阅:运行期自由增减,灵活
- 职责清晰:事件产生与事件响应彻底分离
缺点(务必背熟,面试一定会被追问)
- 通知顺序不确定:模式不保证观察者执行先后
- 可能触发连锁更新 :观察者里再改状态会引发新的通知 → 循环依赖/栈溢出风险
- 难以精确控制调用栈:异步化后更难排查"谁通知了谁"
- 测试复杂度上升:观察者多时,验证需要桩化(mock)多个对象
- 内存泄漏风险:观察者注册后忘记反注册,Subject 长期持有引用 → 对象无法回收
- 异常传染(不做隔离时):一个观察者抛异常打断整条通知链(见 5.2)
诚实标注 :第 6 点是我基于"通知循环未做 try-catch"的确定性推断,已在 5.2 实测证实;其余 1-5 点为该模式的通用共识,非本文实测。
九、面试:亮点分级 + 追问应答链
按你的要求诚实分级------别把"会背定义"当成亮点。
🟢 骨架级(大多数人能达到,不算亮点)
你能说:定义、四个角色、优点缺点、会写个 demo。
追问 1:观察者模式属于哪类模式?
对象行为型模式。
追问 2:核心解耦靠什么?
靠
List<Observer>+ 面向接口编程,Subject 不依赖具体实现。
结论 :这些是底线,面试官不会因此加分。要说"我用了观察者模式",至少要过下面这层。
🟡 加分区(有真实工程经验,能开始加分)
你能说 :推/拉模式区别、异常隔离、线程安全、为什么不用 java.util.Observable。
追问 1:推模式和拉模式怎么选?
数据小而稳定、大家都需要 → 推;数据大、各取所需 → 拉。拿不准就推一个事件对象,观察者自己决定要不要再取。
追问 2:通知循环里一个观察者抛异常怎么办?
单独 try-catch 隔离 + 记日志。因为通知是广播,性质是"尽力送达",不该让单点故障扩散。
追问 3:并发场景怎么保证安全?
观察者列表读多写少,用
CopyOnWriteArrayList;或遍历前List.copyOf()拷贝一份。
追问 4 :JDK 自带的 Observable 能用吗?
不能 ,JDK 9 起
@Deprecated。它继承而非接口、要手动setChanged()、线程不安全。实测编译就有警告。
🔴 深水区(能讲出踩过的坑 + 有边界的思考,明显加分)
你能说:反注册失效的静默 bug、循环依赖、观察者 vs 发布订阅、模式的适用边界。
追问 1(杀手题):你用观察者模式时踩过什么坑?
反注册失效 :
detach(new Observer(...))传新实例、且没重写equals,List.remove()走引用相等 → 移除失败但不报错 ,静默地一直发通知(典型表现:"用户都退订了还在收短信")。解法:持有同一引用 / 重写equals+hashCode/ 用Map<String, Observer>按 key 管理。这条我实测过,三种写法同框验证过。
追问 2:观察者模式会不会导致循环通知?
会。A 通知 B,B 的更新里又改了自己状态并通知回 A → 无限递归。缓解办法:通知里加"变更版本号/时间戳"判重、把通知改成异步批量、或干脆用事件总线切断直接引用。(此问为通用推导,本文未实测)
追问 3:观察者和发布订阅区别?
观察者进程内直连 ,Subject 直接持有 Observer 引用;发布订阅中间多一个 Broker ,双方不感知,可跨进程,通常异步并带重试/持久化。一句话:观察者是直接喊人,发布订阅是发群里。
追问 4 :什么时候你会故意不用观察者模式?
① 只有一个观察者;② 需要严格事务边界(通知难和事务对齐);③ 需要可靠投递/重试/削峰(该用 MQ);④ 观察者之间有复杂顺序依赖(模式不定义顺序)。能说清"什么时候不用",比会用更显水平。
追问 5:生产环境你还会加什么?
① 异常隔离;② COW 或遍历拷贝;③ 反注册的正确姿势;④ 通知去抖/防风暴(版本号判重);⑤ 关键通知考虑异步化 + 落库兜底。④⑤ 属于通用工程建议,本文未实测。
🎯 一句话自评定位
| 你的情况 | 定位 |
|---|---|
| 能背出定义和四个角色 | 🟢 骨架级 |
| 能讲推/拉、异常隔离、COW | 🟡 加分区 |
| 能主动讲反注册的坑并给出三种解法 | 🔴 深水区 |
| 能讲清"什么时候不用"和"循环通知怎么防" | 🔴 深水区(上限) |
十、速记卡(面试前 30 秒扫一遍)
定义:一对多依赖,状态变化自动通知
角色:Subject(持有列表+遍历通知)/ Observer(update 接口)
本质:把硬编码的扇出调用 → 面向接口的广播
两种:推(塞数据)/ 拉(给信号自己取)
加固:异常隔离 + CopyOnWriteArrayList + 反注册姿势
坑: detach(new X()) 没 equals → 静默失效(我实测过)
废弃:java.util.Observable,JDK 9 起 @Deprecated
对比:观察者=进程内直连;发布订阅=多一个 Broker、可跨进程
不用:单观察者 / 强事务 / 要可靠投递(用MQ) / 有顺序依赖
附录 A:实测原始输出(JDK 21.0.12.1,javac 编译通过)
========== Demo0: 反注册的坑(实测翻车点) ==========
[坑A] 用【新实例】反注册 + 未重写 equals → 移除失败
[邮件] wang@x.com 收到:Order{A000, 测试}
↑ 邮件仍然收到 = detach 没生效
[坑B] 用【同一个引用】反注册 → 成功
↑ 邮件没收到 = detach 生效
[坑C] 重写 equals/hashCode 后,新实例也能移除
↑ 邮件没收到 = equals 让 detach 生效
========== Demo1: 最小骨架(推模式) ==========
[短信] 138****0001 收到:Order{A1001, 已支付}
[邮件] zhang@x.com 收到:Order{A1001, 已支付}
--- 新增观察者后再发一次 ---
[短信] 138****0001 收到:Order{A1001, 已发货}
[邮件] zhang@x.com 收到:Order{A1001, 已发货}
[短信] 139****0002 收到:Order{A1001, 已发货}
--- 用同一引用反注册后再发(邮件应消失) ---
[短信] 138****0001 收到:Order{A1001, 已签收}
↑ 只剩短信收到,邮件已反注册
========== Demo2: 异常隔离(坏观察者不拖垮别人) ==========
[短信] 138****0001 收到:Order{B2002, 退款中}
[隔离] 观察者 坏观察者 异常被吞:连接池耗尽
[邮件] li@x.com 收到:Order{B2002, 退款中}
========== Demo3: 拉模式(观察者自己取数) ==========
[拉模式] 自己去 subject 取到:Order{C3003, 已完成}
[短信] 137****0003 收到:Order{C3003, 已完成}
========== Demo4: 并发通知(COW 遍历安全) ==========
[短信] user0 收到:Order{D4004, 并发更新}
[短信] user1 收到:Order{D4004, 并发更新}
[短信] user2 收到:Order{D4004, 并发更新}
并发下无 ConcurrentModificationException
额外事实核验:
Observable 类上注解: @java.lang.Deprecated(forRemoval=false, since="9")
Observer 接口上注解: @java.lang.Deprecated(forRemoval=false, since="9")
JDK 版本: 21.0.12.1
附录 B:完整可运行源码
单文件,保存为
ObserverDemo.java,javac ObserverDemo.java && java ObserverDemo即可复现上面全部输出。
java
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
public class ObserverDemo {
// ---------- 领域模型 ----------
static class Order {
final String id;
String status;
Order(String id, String status) { this.id = id; this.status = status; }
@Override public String toString() { return "Order{" + id + ", " + status + "}"; }
}
// ---------- 观察者接口 ----------
interface Observer {
void update(Order order); // 推模式:被观察者直接把数据塞给你
String name();
}
// ================= 最小骨架(推模式) =================
static class SimpleSubject {
private final List<Observer> observers = new CopyOnWriteArrayList<>();
private Order latest;
public void attach(Observer o) { observers.add(o); }
public void detach(Observer o) { observers.remove(o); }
public void changeStatus(Order o) {
this.latest = o;
notifyAllObservers();
}
private void notifyAllObservers() {
for (Observer o : observers) {
o.update(latest); // 推:直接给数据
}
}
}
// ================= 异常隔离 + 防御性遍历 =================
static class SafeSubject {
private final List<Observer> observers = new CopyOnWriteArrayList<>();
private Order latest;
public void attach(Observer o) { observers.add(o); }
public void detach(Observer o) { observers.remove(o); }
public void changeStatus(Order o) {
this.latest = o;
// 即使不用 COW,也先拷贝一份再遍历,避免遍历中增删
for (Observer o2 : List.copyOf(observers)) {
try {
o2.update(latest);
} catch (Exception e) {
// 关键:单个观察者炸了,不能拖垮整条通知链
System.out.println(" [隔离] 观察者 " + o2.name() + " 异常被吞:" + e.getMessage());
}
}
}
public Order getLatest() { return latest; } // 拉模式取数入口
}
// ---------- 具体观察者 ----------
static class SmsObserver implements Observer {
private final String who;
SmsObserver(String who) { this.who = who; }
public void update(Order order) {
System.out.println(" [短信] " + who + " 收到:" + order);
}
public String name() { return "短信(" + who + ")"; }
}
// 不重写 equals ------ 专门用来演示"反注册失效"的坑
static class EmailObserver implements Observer {
private final String who;
EmailObserver(String who) { this.who = who; }
public void update(Order order) {
System.out.println(" [邮件] " + who + " 收到:" + order);
}
public String name() { return "邮件(" + who + ")"; }
}
// 重写 equals/hashCode ------ 新实例也能被正确移除
static class SafeEmailObserver implements Observer {
private final String who;
SafeEmailObserver(String who) { this.who = who; }
public void update(Order order) {
System.out.println(" [邮件✓] " + who + " 收到:" + order);
}
public String name() { return "邮件✓(" + who + ")"; }
@Override public boolean equals(Object o) {
return o instanceof SafeEmailObserver e && e.who.equals(this.who);
}
@Override public int hashCode() { return who.hashCode(); }
}
// 会抛异常的坏观察者 ------ 用来验证隔离
static class BrokenObserver implements Observer {
public void update(Order order) { throw new IllegalStateException("连接池耗尽"); }
public String name() { return "坏观察者"; }
}
// 拉模式观察者:只收到"有变化"的信号,自己回头取数据
static class PullObserver implements Observer {
private final SafeSubject subject;
PullObserver(SafeSubject subject) { this.subject = subject; }
public void update(Order ignored) {
Order o = subject.getLatest();
System.out.println(" [拉模式] 自己去 subject 取到:" + o);
}
public String name() { return "拉模式观察者"; }
}
public static void main(String[] args) {
System.out.println("========== Demo0: 反注册的坑(实测翻车点) ==========");
System.out.println(" [坑A] 用【新实例】反注册 + 未重写 equals → 移除失败");
SimpleSubject s0 = new SimpleSubject();
s0.attach(new EmailObserver("wang@x.com"));
s0.detach(new EmailObserver("wang@x.com")); // 同内容,但是新对象
s0.changeStatus(new Order("A000", "测试"));
System.out.println(" ↑ 邮件仍然收到 = detach 没生效");
System.out.println(" [坑B] 用【同一个引用】反注册 → 成功");
SimpleSubject s1 = new SimpleSubject();
EmailObserver ref = new EmailObserver("wang@x.com");
s1.attach(ref);
s1.detach(ref); // 同一引用
s1.changeStatus(new Order("A001", "测试"));
System.out.println(" ↑ 邮件没收到 = detach 生效");
System.out.println(" [坑C] 重写 equals/hashCode 后,新实例也能移除");
SafeSubject s2 = new SafeSubject();
s2.attach(new SafeEmailObserver("wang@x.com"));
s2.detach(new SafeEmailObserver("wang@x.com")); // 新实例,靠 equals 命中
s2.changeStatus(new Order("A002", "测试"));
System.out.println(" ↑ 邮件没收到 = equals 让 detach 生效");
System.out.println();
System.out.println("========== Demo1: 最小骨架(推模式) ==========");
SimpleSubject subject = new SimpleSubject();
subject.attach(new SmsObserver("138****0001"));
subject.attach(new EmailObserver("zhang@x.com"));
Order o1 = new Order("A1001", "已支付");
subject.changeStatus(o1);
System.out.println(" --- 新增观察者后再发一次 ---");
subject.attach(new SmsObserver("139****0002"));
subject.changeStatus(new Order("A1001", "已发货"));
System.out.println(" --- 用同一引用反注册后再发(邮件应消失) ---");
EmailObserver mailRef = new EmailObserver("zhang@x.com");
SimpleSubject s3 = new SimpleSubject();
s3.attach(new SmsObserver("138****0001"));
s3.attach(mailRef);
s3.detach(mailRef);
s3.changeStatus(new Order("A1001", "已签收"));
System.out.println(" ↑ 只剩短信收到,邮件已反注册");
System.out.println();
System.out.println("========== Demo2: 异常隔离(坏观察者不拖垮别人) ==========");
SafeSubject safe = new SafeSubject();
safe.attach(new SmsObserver("138****0001"));
safe.attach(new BrokenObserver());
safe.attach(new EmailObserver("li@x.com"));
safe.changeStatus(new Order("B2002", "退款中"));
System.out.println();
System.out.println("========== Demo3: 拉模式(观察者自己取数) ==========");
SafeSubject pullSub = new SafeSubject();
pullSub.attach(new PullObserver(pullSub));
pullSub.attach(new SmsObserver("137****0003"));
pullSub.changeStatus(new Order("C3003", "已完成"));
System.out.println();
System.out.println("========== Demo4: 并发通知(COW 遍历安全) ==========");
SafeSubject conc = new SafeSubject();
for (int i = 0; i < 3; i++) conc.attach(new SmsObserver("user" + i));
Thread t = new Thread(() -> conc.changeStatus(new Order("D4004", "并发更新")));
t.start();
try { t.join(); } catch (InterruptedException ignored) {}
System.out.println(" 并发下无 ConcurrentModificationException");
}
}