Java 观察者模式(Observer)深入解析 —— 基于 CS-Notes 设计模式笔记与完整源码实现
观察者(Observer)模式是 GoF 23 种设计模式中最为常用的行为型模式之一,它定义了对象之间一对多的依赖关系:当一个对象(主题 Subject)的状态发生改变时,所有依赖它的对象(观察者 Observer)都会自动收到通知并随之更新。本篇文章以 CS-Notes 仓库中 设计模式 - 观察者.md 为核心骨架,配合天气数据布告板(Weather Station)的完整 Java 实现逐行剖析接口定义、注册/移除/通知机制,并结合仓库中消息队列.md对观察者模式与发布-订阅模式的辨析、JDK 内置支持等内容展开纵深讲解。读完本文,你将能够独立理解观察者模式的结构语义、徒手实现一个可扩展的一对多通知系统,并能准确判断在哪些 JDK 与框架机制背后隐藏着该模式的影子。
一、核心意图(Intent):一对多依赖与自动通知
观察者模式要解决的经典问题是"一处状态变化,多处联动刷新"。在 CS-Notes 的原文中,它的意图被精炼地概括为:
定义对象之间的一对多依赖,当一个对象状态改变时,它的所有依赖都会收到通知并且自动更新状态。
模式中涉及两个核心角色:
- 主题(Subject):被观察的对象,也就是"持有状态"的一方。图中左侧的 Subject Object 正是"Object that holds state";
- 观察者(Observer):依赖者,当主题状态变化时需要同步更新的对象。图中右侧区域被标注为 "Dependent Objects"。
上图直观展示了这种关系:一个 Subject Object 面向多个观察者对象(Dog、Duck、Cat、Mouse),通过自动的 "update/notification" 完成状态同步。这里的关键在于"自动"——主题不需要关心观察者内部如何处理状态,只需在自身状态变更后发出通知即可。
从职责上看,主题对被观察的状态与观察者的注册表负责,观察者对自己的更新逻辑负责,两者通过接口解耦,从而使依赖关系可以动态增减而不影响已有代码。
二、模式结构与角色职责
在标准实现中,主题(Subject)被设计为接口,约定三类能力:
- 注册观察者:新增一个订阅方;
- 移除观察者:取消一个订阅方;
- 通知观察者:状态变更时遍历观察者列表并逐一触发其更新逻辑。
观察者(Observer)同样被抽象为接口,声明一个 update() 更新入口。主题正是通过"维护一张观察者列表"来实现注册、移除与通知这三种操作的。
从类图上可以看到两条清晰的依赖链:
- Subject(抽象接口) —— 提供
registerObserver()、removeObserver()、notifyObservers()三个方法签名,ConcreteSubject(具体主题)负责实现并真正持有观察者集合与业务状态; - Observer(抽象接口) —— 提供
update()方法签名,ConcreteObserver(具体观察者)实现收到通知后的实际行为。
结合原文注释可以明确一点:观察者的注册行为需要由观察者自己主动调用主题的 registerObserver() 方法完成,这一"观察者自注册"的设计在后面的天气站示例中会反复出现。
三、完整 Java 实现:天气数据布告板
CS-Notes 采用了一个经典到几乎成为观察者模式代名词的场景——天气数据布告板(Weather Station):天气数据一旦发生改变,多个布告板就要同步刷新,并且布告板的数量将来还可能继续增加。
正如图中所示,"当前状况""天气统计""预报"等布告板关注同一份天气数据的不同侧面。若把刷新逻辑直接写死进天气数据类,每新增一种布告板都要改动数据源代码——观察者模式正是为了把这个"将来会继续增加"的扩展点彻底打开。
3.1 主题接口 Subject
public interface Subject {
void registerObserver(Observer o);
void removeObserver(Observer o);
void notifyObserver();
}
3.2 具体主题:WeatherData
WeatherData 实现了 Subject,内部维护一张 List<Observer> 观察者列表,同时持有温度、湿度、气压三个天气数据字段:
public class WeatherData implements Subject {
private List<Observer> observers;
private float temperature;
private float humidity;
private float pressure;
public WeatherData() {
observers = new ArrayList<>();
}
public void setMeasurements(float temperature, float humidity, float pressure) {
this.temperature = temperature;
this.humidity = humidity;
this.pressure = pressure;
notifyObserver();
}
@Override
public void registerObserver(Observer o) {
observers.add(o);
}
@Override
public void removeObserver(Observer o) {
int i = observers.indexOf(o);
if (i >= 0) {
observers.remove(i);
}
}
@Override
public void notifyObserver() {
for (Observer o : observers) {
o.update(temperature, humidity, pressure);
}
}
}
结合原文可以提炼出这段实现的几个关键设计点:
- 数据更新即通知触发:
setMeasurements(...)在写入新测量值后立即调用notifyObserver(),保证"数据一变、通知即达"的时序一致性,这也是主题在状态变更时唯一需要主动做的事; - 注册表驱动通知:
notifyObserver()通过for循环遍历observers列表,逐个调用o.update(temperature, humidity, pressure),把最新数据以参数形式推送给所有观察者; - 安全的移除:
removeObserver()先通过indexOf(o)定位下标,只有i >= 0(即观察者确实存在)时才执行移除,避免对不存在的元素误删; - 面向接口编程:
WeatherData只依赖Observer接口而非具体布告板类,因此新增布告板时无需改动 WeatherData 的任何代码——这正是"主题与观察者解耦"带来可扩展性的直接体现。
3.3 观察者接口 Observer
public interface Observer {
void update(float temp, float humidity, float pressure);
}
3.4 具体观察者:两种布告板
StatisticsDisplay 与 CurrentConditionsDisplay 是两种不同诉求的布告板,它们的结构完全同构:在构造器中接收主题对象并立刻调用 registerObserver(this) 完成自注册,随后在 update() 中定义各自的刷新行为。
public class StatisticsDisplay implements Observer {
public StatisticsDisplay(Subject weatherData) {
weatherData.registerObserver(this);
}
@Override
public void update(float temp, float humidity, float pressure) {
System.out.println("StatisticsDisplay.update: " + temp + " " + humidity + " " + pressure);
}
}
public class CurrentConditionsDisplay implements Observer {
public CurrentConditionsDisplay(Subject weatherData) {
weatherData.registerObserver(this);
}
@Override
public void update(float temp, float humidity, float pressure) {
System.out.println("CurrentConditionsDisplay.update: " + temp + " " + humidity + " " + pressure);
}
}
值得指出的是"构造器自注册"这一惯用法:观察者的构造函数接收 Subject 类型参数并主动订阅,从而把"谁订阅谁"的绑定关系收敛到观察者一侧。之后如果某个布告板不再需要数据,只需要让代码持有其引用并调用 weatherData.removeObserver(display) 即可从通知链上摘除。
3.5 客户端组装:WeatherStation
public class WeatherStation {
public static void main(String[] args) {
WeatherData weatherData = new WeatherData();
CurrentConditionsDisplay currentConditionsDisplay = new CurrentConditionsDisplay(weatherData);
StatisticsDisplay statisticsDisplay = new StatisticsDisplay(weatherData);
weatherData.setMeasurements(0, 0, 0);
weatherData.setMeasurements(1, 1, 1);
}
}
运行 WeatherStation 后控制台输出如下(与原文一致):
CurrentConditionsDisplay.update: 0.0 0.0 0.0
StatisticsDisplay.update: 0.0 0.0 0.0
CurrentConditionsDisplay.update: 1.0 1.0 1.0
StatisticsDisplay.update: 1.0 1.0 1.0
输出序列验证了两个事实:其一,setMeasurements() 的每次调用都会把全部已注册观察者各通知一遍,且通知顺序与注册顺序一致(先 Current 后 Statistics);其二,观察者无须轮询数据源,数据更新的主动权完全由主题掌控,这正是观察者模式与"主动拉取"方案的本质区别。
四、观察者模式 vs 发布-订阅模式与消息队列
许多初学者会把观察者模式和消息队列的"发布/订阅"模型混为一谈。CS-Notes 仓库在消息队列.md中专门对二者做了辨析,其原文观点如下:
- 观察者模式中,观察者和主题都知道对方的存在;而在发布与订阅模式中,生产者与消费者不知道对方的存在,它们之间通过频道进行通信。
- 观察者模式是同步的,当事件触发时,主题会调用观察者的方法,然后等待方法返回;而发布与订阅模式是异步的,生产者向频道发送一个消息之后,就不需要关心消费者何时去订阅这个消息,可以立即返回。
从这份对比可以看出两个维度的差异:
| 维度 | 观察者模式 | 发布-订阅 / 消息队列 |
|---|---|---|
| 参与方耦合 | 主题与观察者互相知晓,观察者显式向主题注册 | 生产者与消费者彼此无感,通过中间频道(Channel/Queue)解耦 |
| 通信方式 | 同步回调:主题调用观察者方法并等待返回 | 异步:发送消息后即可返回,消费时机由消费者决定 |
| 扩展形式 | 一对多的直接依赖通知 | 消息可被点对点消费一次,也可被多个订阅者各自消费 |
正因如此,观察者模式通常用于同一进程内的组件间同步联动(例如 GUI 组件刷新、数据模型与视图同步),而消息队列适用于跨进程、异步、削峰的解耦场景。仓库消息队列.md同时给出了一个很形象的判断标准:像"注册后必须点击验证邮件才能完成注册"这类强同步业务,就不能简单替换为异步消息处理。理解这层区别,有助于在系统设计时选择正确的解耦手段。
五、JDK 与生态中的观察者模式应用
CS-Notes 原文在 "JDK" 一节指出了观察者模式在标准库与主流框架中的典型落点,它们是对"模式如何被工业界使用"的最好注解:
java.util.Observer/java.util.Observable:JDK 1.0 就内置的观察者接口与主题类,开发者可继承Observable、实现Observer快速搭建通知机制。值得注意的是,该方案存在设计上的历史包袱(Observable是类而非接口、需要继承使用等),从 Java 9 起已被标记为弃用(deprecated),新代码更推荐自行按接口方式实现,或在 UI 框架中使用监听器机制。上面天气站的实现本质上就是对它的"接口化"改良。java.util.EventListener:Java 事件模型(如 Swing/AWT)中所有监听器接口的标记父接口。Swing 的ActionListener、MouseListener等典型用法正是观察者模式:按钮(主题)维护监听器列表,用户点击(状态变化)时通知所有已注册监听器。javax.servlet.http.HttpSessionBindingListener:Servlet 规范中的会话绑定监听器。当对象被放入或移出HttpSession(绑定/解绑事件发生)时,容器会回调该监听器对应方法,属于观察者思想在 Web 容器生命周期管理中的运用。- RxJava:响应式编程库。
Observable(被观察者)与Observer(观察者)是其核心抽象,其"上游发射事件、下游订阅响应"的模型可视作观察者模式在异步数据流与背压维度上的现代化演进。
除上述条目外,观察者模式在 Java 生态中几乎无处不在:Swing 的事件分发模型、JavaBeans 的 PropertyChangeListener(属性变更即通知)、各类 EventBus 事件总线等,底层逻辑都能回溯到本文讲解的"注册—移除—通知"三件套。
六、实践要点与常见陷阱
结合天气站的完整代码,可以把落地观察者模式的要点总结如下:
- 围绕接口而非具体类编程:主题只依赖
Observer接口,观察者只依赖Subject接口。这样主题不感知布告板具体类型,新增布告板(如 ForecastDisplay)对现有代码零侵入。 - 注册时机收敛:像示例那样在观察者构造器中调用
registerObserver(this),可以有效避免"创建了观察者却忘记订阅"的疏漏。 - 同步回调的性能与异常考量:本模式默认是同步遍历调用,若某个观察者的
update()耗时较长或抛出异常,会阻塞后续观察者的通知甚至中断整个遍历。工业实现中通常需要对观察者列表做异常隔离,并评估是否需要引入异步通知(异步化即向发布-订阅模型靠拢,可参考上节对比)。 - 注意观察者的生命周期:被移除的对象若仍被主题的列表持有引用,将造成内存泄漏,因此在观察者销毁时务必调用
removeObserver()完成"退订"。 - 信息推送的粒度:天气站的实现把
temperature/humidity/pressure三个值全部作为update()参数"推"给观察者;当状态对象字段较多时,也可以考虑只推送一个事件对象引用,由观察者自行按需读取(即"推/拉"两种模型的选择)。
七、在 CS-Notes 设计模式体系中的位置
在 CS-Notes 的设计模式分类中,观察者属于行为型模式,与责任链、命令、状态、策略、模板方法、访问者等并列。其完整索引见 设计模式 - 目录.md,该目录的前言指出:"设计模式是解决问题的方案,学习现有的设计模式可以做到经验复用。拥有设计模式词汇,在沟通时就能用更少的词汇来讨论,并且不需要了解底层细节。"观察者模式正是这一定义的典型代表——一旦团队掌握了"观察者"这一词汇,"某个数据源变化后所有界面要刷新"这类需求就能用最简洁的语言准确传达。
进一步地,仓库中与之关联的主题还包括:
- 消息队列.md:观察者模式与发布-订阅模式、异步处理的详细对比(上文第四节即由此展开);
- 面向对象思想.md 与 Java 基础.md:理解"面向接口编程""组合优于继承"等设计原则,有助于从底层思路上把握观察者模式为何如此组织接口与依赖。
整体上看,观察者模式用最小的接口契约(注册、移除、通知 + 一个更新入口)换取了主题与观察者之间最大程度的松耦合,是"开闭原则"在行为扩展上最直观的体现之一。在需要"一处变更、多处联动"且联动方数量可能持续增长的场景中,它就是那个值得优先考虑的成熟方案。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0627
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00


