在构建现代 Java 应用程序时,我们经常面临着这样一个挑战:当对象的状态发生改变时,如何高效地通知其他依赖对象?这种“一对多”的依赖关系在软件开发中非常普遍,比如图形用户界面(GUI)更新、股票价格监控或即时通讯系统。为了解决这个问题,Java 为我们提供了一个经典的工具——java.util.Observable 类。
在这篇文章中,我们将深入探讨 Java 中的 Observable 类。我们将了解它是如何工作的,如何通过它实现观察者模式,以及为什么在使用它时需要格外小心(特别是在 Java 9 之后)。无论你是正在准备面试,还是试图维护遗留系统,或者只是想理解现代响应式编程的鼻祖,这篇文章都将为你提供全方位的知识梳理。
观察者模式的核心概念
Observable 类是 Java 实现观察者模式的关键组成部分。简单来说,它允许我们将对象分为两类:
- 被观察者:它是数据的拥有者或状态的维护者。正如其名,它是“被监视”的对象。
- 观察者:它对被观察者的状态变化感兴趣。一旦被观察者发生变化,观察者就会收到通知并采取行动。
这种机制实现了一种松耦合的设计。被观察者不需要知道具体的观察者是谁,它只需要知道“有一群对象在关注我”。这极大地提高了代码的可维护性和灵活性。
> 注意: 虽然这是一个经典的模式,但我们必须提到,从 Java 9 开始,INLINECODE87f9c218 类和 INLINECODE8a1c9fd0 接口已经被标记为Deprecated(过时)。这是因为在并发场景下它存在一些难以解决的线程安全问题,而且事件模型的灵活性也不如现代的响应式流。但在学习设计模式和理解旧系统架构时,它依然具有极高的参考价值。
如何使用 Observable 类:基本规则
要想让这套机制运转起来,我们需要遵循两条核心规则,分别针对两类对象。
#### 1. 被观察者的职责
一个类要想被观察,它必须继承 Observable 类。当你在这个子类对象中发生变更时,必须遵循一个特定的流程,否则通知不会生效。这通常被形象地称为“修改-通知”循环。
- 标记变更 (INLINECODEf9533f7e):这是最关键的一步。当你修改了数据(比如温度计的读数变了),你必须首先调用 INLINECODEfb409cce 方法。这个方法会在内部设置一个标志位(
changed = true),告诉系统“数据确实变了”。 - 通知观察者 (INLINECODE123fbcd9):标记变更后,当你准备好通知大家时,调用 INLINECODE4c20f540。这会触发所有已注册观察者的
update()方法。
重要警告: 如果你在修改数据后忘记调用 INLINECODE6041ed96,直接调用了 INLINECODEac9126c4,那么什么都不会发生。观察者不会收到通知,系统会认为这次修改无效。这是一个非常常见的初学者错误。
#### 2. 观察者的职责
观察者类必须实现 Observer 接口。这个接口只要求你重写一个方法:
void update(Observable o, Object arg)
每当被观察者调用 INLINECODE6631e753 时,这个 INLINECODE764a49ee 方法就会被调用。
Observable 类的构造函数
Observable 类的设计非常简单,它只提供了一个构造函数,确保我们在创建对象时一切从零开始。
构造函数:Observable()
- 作用:构造一个新的拥有零个观察者的 Observable 对象。
- 使用场景:通常你会在自定义类的构造函数中隐式调用它(因为 Java 会自动调用父类的无参构造函数),或者如果你直接使用 Observable,只需
new Observable()即可。
核心方法详解与代码实战
现在,让我们深入探讨 Observable 类中最常用的几个方法,并通过代码来看看它们到底是如何工作的。
#### 1. addObserver(Observer observer):注册观察者
在观察者能够收到消息之前,它必须先“订阅”被观察者。
方法签名:
public void addObserver(Observer observer)
- 参数:
observer– 也就是想要接收通知的观察者对象。 - 返回类型:无。
工作原理:
这个方法会将传入的观察者对象添加到一个内部的集合中。Observable 负责管理这个列表,确保通知能够按顺序发送。
实战示例:
让我们看一个简单的例子。我们要创建一个“新闻发布室”,它可以发布新闻,而“订阅者”会收到通知。
import java.util.*;
// 定义一个订阅者(观察者)
class NewsSubscriber implements Observer {
private String name;
public NewsSubscriber(String name) {
this.name = name;
}
@Override
public void update(Observable obj, Object arg) {
// arg 参数通常包含具体的更新内容
System.out.println("Hi " + name + ", 收到新消息: " + arg);
}
}
// 定义新闻发布室(被观察者)
class NewsAgency extends Observable {
void publishNews(String newsContent) {
// 1. 标记状态已更改
setChanged();
// 2. 通知所有观察者,并传递消息内容
notifyObservers(newsContent);
}
}
public class ObserverDemo {
public static void main(String[] args) {
// 创建被观察者
NewsAgency agency = new NewsAgency();
// 创建观察者
NewsSubscriber sub1 = new NewsSubscriber("Alice");
NewsSubscriber sub2 = new NewsSubscriber("Bob");
// 注册观察者
agency.addObserver(sub1);
agency.addObserver(sub2);
// 发布新闻
System.out.println("--- 发布第一条新闻 ---");
agency.publishNews("Java 21 正式发布!");
System.out.println("
--- 发布第二条新闻 ---");
agency.publishNews("观察者模式详解");
}
}
输出结果:
--- 发布第一条新闻 ---
Hi Bob, 收到新消息: Java 21 正式发布!
Hi Alice, 收到新消息: Java 21 正式发布!
--- 发布第二条新闻 ---
Hi Bob, 收到新消息: 观察者模式详解
Hi Alice, 收到新消息: 观察者模式详解
解析:你可以看到,Bob 和 Alice 都收到了消息。值得注意的是,通知的顺序与添加的顺序相反(后添加的先收到),这是因为在 Vector 实现中,它是从尾部开始遍历的。
#### 2. setChanged():更改的“开关”
这个方法是 Observable 机制中最微妙的部分。
方法签名:
protected void setChanged()
- 参数:无。
- 返回类型:无。
为什么我们需要它?
你可能会有疑问:“为什么我不能直接调用 INLINECODE92c9056d?” 答案在于控制权。有时候,你的对象可能会发生很多次微小的变化(比如在一个循环中更新变量),你可能不希望每次赋值都触发一次通知。你可能希望等待所有更新完成后,再一次性通知观察者。INLINECODEba3eb6cc 就像是一个开关,只有当你“打开”它,notifyObservers() 才会起作用。
实战示例:触发条件验证
下面的例子演示了 setChanged() 对通知流程的决定性影响。
import java.util.*;
class WatchedObject extends Observable {
// 场景1:正确调用 setChanged
public void triggerValidUpdate() {
setChanged(); // 标记状态变更
System.out.println("[内部状态] hasChanged 状态: " + hasChanged());
notifyObservers("这是一个有效的更新");
}
// 场景2:忘记调用 setChanged
public void triggerInvalidUpdate() {
// 故意不调用 setChanged()
System.out.println("[内部状态] hasChanged 状态: " + hasChanged());
notifyObservers("你将看不到这条消息");
}
}
class SimpleObserver implements Observer {
@Override
public void update(Observable o, Object arg) {
System.out.println("观察者收到通知: " + arg);
}
}
public class SetChangedDemo {
public static void main(String[] args) {
WatchedObject obj = new WatchedObject();
SimpleObserver obs = new SimpleObserver();
obj.addObserver(obs);
System.out.println("=== 测试 1: 调用 setChanged ===");
obj.triggerValidUpdate();
// 每次通知后,标志位会被自动清除,所以第二次调用还是需要 setChanged
System.out.println("
=== 测试 2: 不调用 setChanged ===");
obj.triggerInvalidUpdate();
}
}
输出结果:
=== 测试 1: 调用 setChanged ===
[内部状态] hasChanged 状态: true
观察者收到通知: 这是一个有效的更新
=== 测试 2: 不调用 setChanged ===
[内部状态] hasChanged 状态: false
解析:注意看测试 2,尽管我们调用了 INLINECODE7db5b32e,但因为没有先调用 INLINECODEd951b325,update 方法根本没有被执行。这就是为什么必须严格遵守流程的原因。
#### 3. clearChanged():重置状态
方法签名:
protected void clearChanged()
- 作用:将对象的“已更改”标志位重置为
false。 - 自动行为:你通常不需要手动调用这个方法,因为 INLINECODE9789d03d 方法在通知完所有观察者后,会自动调用 INLINECODE6352ad58。这确保了同一次修改只会触发一次通知。
使用场景:如果你在某些特殊逻辑中,虽然修改了数据但决定稍后再通知(或者决定取消通知),你可能需要手动清除这个标志。但这比较少见。
实战示例:手动清除状态
import java.util.*;
class SmartObservable extends Observable {
void tryUpdateButCancel() {
setChanged();
System.out.println("状态已标记为更改...");
// 假设这里发生了某些业务逻辑,导致我们决定撤销这次更新通知
clearChanged();
System.out.println("手动清除更改标志: " + hasChanged());
// 这次通知将不会生效,因为标志已被清除
notifyObservers("取消通知");
}
}
class MyObserver implements Observer {
@Override
public void update(Observable o, Object arg) {
System.out.println("收到通知: " + arg);
}
}
public class ClearChangedDemo {
public static void main(String[] args) {
SmartObservable observable = new SmartObservable();
observable.addObserver(new MyObserver());
observable.tryUpdateButCancel();
}
}
输出结果:
状态已标记为更改...
手动清除更改标志: false
观察者的 update 方法未被调用,证实了状态已经被清除。
#### 4. INLINECODEbe7c10cd vs INLINECODEa84b8cb6
通知观察者有两种形式:
- INLINECODEb8fe11da:不传递任何参数。观察者在 INLINECODE75f7daef 方法中接收到的 INLINECODEcd4b07b2 将为 INLINECODE9b420b94。观察者需要主动查询被观察者对象的状态来获取变化。
-
notifyObservers(Object arg):传递一个对象(通常是更新后的数据或具体的消息)。这种方式通常称为“推”模型,被观察者主动把数据推给观察者。
最佳实践建议:
- 如果数据量很大或者变化很复杂,使用无参的
notifyObservers(),让观察者自己去“拉”取数据,这样更节省内存。 - 如果变化只是一个简单的字符串或数值,使用带参的
notifyObservers(arg),代码会写起来更方便。
常见错误与调试技巧
在实际开发中,我们总结了几个大家容易踩的坑,希望能帮你节省调试时间:
- 忘记调用
setChanged():这是排名第一的错误。如果你发现观察者没有反应,90% 的情况是因为你修改了数据但没标记状态。 - 在 INLINECODE4a01062d 中进行耗时操作:INLINECODEd1ca3db2 在默认实现中是同步的。这意味着如果你在 INLINECODE2c95fb90 方法里写了网络请求或复杂的计算,会阻塞被观察者的线程。务必确保 INLINECODEf5f147b5 方法执行迅速,或者将其放入独立线程中执行。
- 内存泄漏:观察者被添加到 Observable 的列表中,但如果不再需要时没有移除,被观察者对象就会持有观察者的引用,导致观察者无法被垃圾回收(GC)。务必在不使用时调用
deleteObserver(Observer o)。
2026 视角:从遗留代码到现代响应式架构
现在我们已经掌握了 INLINECODE7432eeb0 的核心机制。但作为 2026 年的技术专家,我们必须指出它在现代开发中的局限性。你是否想过,为什么我们在企业级开发中越来越少见到原生 INLINECODE1ffdcc76 的身影?
#### 为什么我们需要替代方案?
在构建高性能、分布式的现代应用时,原生 Observable 显得力不从心。主要痛点包括:
- 单继承的噩梦:Java 不支持多重继承。如果你的业务模型继承了
Observable,它就无法再继承其他更有价值的基类,严重限制了代码的扩展性。 - 缺乏背压控制:在处理高速数据流(如 IoT 传感器数据或金融交易流水)时,如果观察者处理速度慢于生产者,原生
Observable会导致内存溢出(OOM)。现代响应式编程(如 Reactive Streams)引入了“背压”机制来解决这个问题。 - 并发模型简陋:它是同步阻塞的。在微服务架构中,我们需要非阻塞、异步的事件驱动模型来保证系统的吞吐量。
#### 现代替代方案:Reactive Streams 与 Project Reactor
在我们的技术栈中,我们通常推荐使用 Java Flow API (Java 9+) 或更强大的 Project Reactor / RxJava。
让我们来看一个对比示例。假设我们正在开发一个实时股票监控系统(这在 2026 年的量化金融中非常常见)。
使用 Reactor (Flux) 的现代实现:
import reactor.core.publisher.Flux;
import java.time.Duration;
// 模拟股票价格流
public class ReactiveStockMonitor {
public static void main(String[] args) throws InterruptedException {
// 创建一个每 500ms 发射一次数据的 Flux (类似 Observable)
Flux.interval(Duration.ofMillis(500))
.map(tick -> "Stock Price Updated at t=" + tick)
// 这里的 doOnNext 类似于 Observer 的 update
.doOnNext(price -> System.out.println("[Subscriber 1] Received: " + price))
// 在不同的线程中异步处理
.subscribeOn(java.util.concurrent.ForkJoinPool.commonPool())
.subscribe();
// 保持主线程存活以观察输出
Thread.sleep(2000);
}
}
这种写法的优势在于:
- 组合式编程:我们可以轻松地使用 INLINECODE1c0f2b9e, INLINECODE241de2af,
.debounce()等操作符来处理数据流。 - 异步非阻塞:不再阻塞主线程,极大提升了系统的并发能力。
- 背压支持:当消费者处理不过来时,可以自动通知生产者降速。
最佳实践总结:什么时候还用 Observable?
虽然我们推崇新技术,但在以下场景中,理解和使用原生 Observable 依然有价值:
- 维护遗留系统:很多大型银行或政府系统依然运行在 Java 8 甚至更早的版本上,彻底重构风险巨大。
- 简单的 GUI 事件:对于一些极其简单的桌面应用,不需要引入 Reactor 这种重型依赖。
- 学习与面试:它是理解观察者模式最直观的教材。
在最近的一个项目中,我们团队面临一个选择:是修复一个基于 INLINECODE601dac18 的旧模块,还是将其重写为响应式风格。最终,我们决定在非核心路径上保留 INLINECODE3c79fe2c,但在核心的数据处理管道中引入了 Reactive Streams。这种渐进式重构策略,帮助我们在保证系统稳定的同时,逐步提升了技术先进性。
希望这篇文章能帮助你更好地理解 Java 中的对象交互机制!如果你在编写代码时遇到问题,不妨回头看看这些规则,往往就能找到答案。