在复杂前端或后端业务里,我们经常需要感知某个集合内部数据的变化,从而自动执行界面渲染、日志埋点或缓存更新。观察者模式提供了一种优雅的思路:把集合自身作为被观察的主题,把关心变化的逻辑封装成观察者,一旦集合发生变动就主动通知。下面以 Java 为例,从零构建一个具备变量变动监听能力的自定义集合。

一、观察者模式的核心角色
观察者模式包含两种基本角色:主题(Subject)和观察者(Observer)。主题维护一份观察者列表,并在自身状态改变时遍历列表调用更新方法;观察者只依赖主题提供的通知接口,不需要主动查询数据。放到自定义集合场景里,集合就是主题,而监听集合变化的回调函数或对象就是观察者。
这种解耦带来的好处是明显的。假设页面有多个模块都依赖同一个用户列表,如果采用轮询方式,每个模块都要定时读取集合内容做比对,既浪费 CPU 又难保证实时性。而使用观察者模式后,集合在 add 或 remove 时立刻广播事件,相关模块各自响应,彼此互不知道对方存在,系统扩展性更强。
1.1 定义观察者接口
我们先抽象出一个通用的监听器接口,它接收集合变动的事件对象。事件里至少应包含操作类型、受影响的元素以及可选的旧值,这样观察者才能做精细化处理。
// 集合变更事件
class CollectionChangeEvent<E> {
public enum Type { ADD, REMOVE, SET }
private final Type type;
private final E element;
private final E oldValue;
public CollectionChangeEvent(Type type, E element, E oldValue) {
this.type = type;
this.element = element;
this.oldValue = oldValue;
}
public Type getType() { return type; }
public E getElement() { return element; }
public E getOldValue() { return oldValue; }
}
// 观察者接口
interface CollectionListener<E> {
void onChanged(CollectionChangeEvent<E> event);
}
上述代码中,CollectionChangeEvent 作为数据载体,把“发生了什么”和“具体数据”打包传递给监听者。使用枚举 Type 区分新增、删除和替换,避免观察者用字符串判断导致拼写错误。
1.2 主题基类设计
主题负责注册、注销观察者,并在合适时机触发通知。我们把这部分逻辑抽出来,方便后续的具体集合类复用。
import java.util.ArrayList;
import java.util.List;
abstract class ObservableCollection<E> {
private final List<CollectionListener<E>> listeners = new ArrayList<>();
public void addListener(CollectionListener<E> l) {
listeners.add(l);
}
public void removeListener(CollectionListener<E> l) {
listeners.remove(l);
}
protected void fireEvent(CollectionChangeEvent.Type type, E element, E oldValue) {
CollectionChangeEvent<E> evt = new CollectionChangeEvent<>(type, element, oldValue);
// 使用副本遍历,防止监听器中移除自身导致并发修改异常
for (CollectionListener<E> l : new ArrayList<>(listeners)) {
l.onChanged(evt);
}
}
}
注意 fireEvent 方法里用 new ArrayList 拷贝了监听器列表再遍历。这是因为监听器在收到通知时可能会调用 removeListener 注销自己,如果直接遍历原列表会抛出 ConcurrentModificationException。这种细节在自定义集合里非常关键。
二、实现可监听的自定义列表
有了基类,我们可以写一个具体的 ObservableList,内部持有普通的 ArrayList,并包装常用方法。每次修改都先改底层数据,再发事件。
2.1 包装增删改方法
下面代码展示如何重写 add、remove 和 set 方法,让它们在完成数据操作后主动通知观察者。
import java.util.ArrayList;
class ObservableList<E> extends ObservableCollection<E> {
private final ArrayList<E> delegate = new ArrayList<>();
public boolean add(E e) {
boolean ok = delegate.add(e);
if (ok) {
fireEvent(CollectionChangeEvent.Type.ADD, e, null);
}
return ok;
}
public E remove(int index) {
E old = delegate.remove(index);
fireEvent(CollectionChangeEvent.Type.REMOVE, old, null);
return old;
}
public E set(int index, E e) {
E old = delegate.set(index, e);
fireEvent(CollectionChangeEvent.Type.SET, e, old);
return old;
}
public E get(int index) {
return delegate.get(index);
}
public int size() {
return delegate.size();
}
}
在 set 方法中,我们既传了新值也传了旧值。这样观察者就能知道某个位置从什么变成了什么,比如做差值同步时非常有用。remove 只传旧值,因为元素已经不在集合里了。
这种实现方式对调用方完全透明:业务代码仍然像使用普通 List 一样调用 add 或 set,只是额外注册了一个监听器就能获得变动提醒,没有侵入原有逻辑。
2.2 注册监听器并使用
实际使用时,只需创建集合对象,添加监听器,然后执行变动操作,监听器就会打印或处理事件。
public class Demo {
public static void main(String[] args) {
ObservableList<String> list = new ObservableList<>();
list.addListener(evt -> {
switch (evt.getType()) {
case ADD:
System.out.println("新增: " + evt.getElement());
break;
case REMOVE:
System.out.println("删除: " + evt.getElement());
break;
case SET:
System.out.println("修改: " + evt.getOldValue() + " -> " + evt.getElement());
break;
}
});
list.add("苹果");
list.add("香蕉");
list.set(1, "橙子");
list.remove(0);
}
}
运行后会依次输出新增、修改、删除的提示。可以看到,监听逻辑与数据操作分离,如果以后要增加“变动写日志”的需求,只需再添加一个监听器,不必改集合本身的代码。
三、进阶考量与避坑
自定义可监听集合在简单场景下很好用,但生产环境还要注意几个问题,否则容易引发隐蔽 Bug。
3.1 防止通知期间的重入修改
如果某个监听器在收到 ADD 事件后又往同一个集合 add 元素,就会再次触发 fireEvent,形成递归调用甚至栈溢出。可以在 fireEvent 时加一个布尔标记,发现正在通知就暂存变更或抛异常,视业务而定。
abstract class SafeObservableCollection<E> extends ObservableCollection<E> {
private boolean notifying = false;
@Override
protected void fireEvent(CollectionChangeEvent.Type type, E element, E oldValue) {
if (notifying) {
// 简单处理:忽略重入通知,或加入队列延后处理
return;
}
notifying = true;
try {
super.fireEvent(type, element, oldValue);
} finally {
notifying = false;
}
}
}
上面用 notifying 标志保证同一时刻只发一轮通知。若业务必须支持重入,可把嵌套的事件放进队列,等本轮结束再补发,但复杂度会明显上升,需权衡。
3.2 弱引用监听器避免内存泄漏
如果监听器是匿名内部类且长期持有外部对象,而集合生命周期比外部对象长,就会造成内存泄漏。可用 WeakReference 包装监听器,或者提供明确的 removeListener 调用点。在 Android 或长期运行的服务中这一点尤为重要。
| 方案 | 优点 | 缺点 |
|---|---|---|
| 强引用+手动注销 | 通知及时、逻辑简单 | 易忘写注销导致泄漏 |
| 弱引用自动回收 | 无需手动管理 | 监听器可能意外被 GC |
表格对比了两种常见做法。团队可根据集合归属关系选择:若是局部临时集合,强引用配合 try-finally 注销最直观;若是全局单例集合,弱引用更稳妥。
四、总结
通过观察者模式实现自定义集合,本质是把“数据变动”抽象成事件,并用接口隔离通知与处理逻辑。核心步骤包括定义事件与监听器接口、在主题中维护监听器列表、在集合写操作时触发通知,以及处理并发修改和内存泄漏等边界情况。掌握这套思路后,你不仅能写出可监听的 List,也可以轻松扩展到 Map、Set 甚至配置对象的属性级监听,让业务模块之间的协作更加清晰。