Java理论与实践:做个好的(事件)侦听器2010-12-20 IBM Brian Goetz观察者模式在 Swing 开发中很常见,在 GUI 应用程序以外的场景中,它对 于消除组件的耦合性也非常有用。但是,仍然存在一些侦听器登记和调用方面的 常见缺陷。在 Java 理论与实践 的这一期中,Java 专家 Brian Goetz 就如何 做一个好的侦听器,以及如何对您的侦听器也友好,提供了一些感觉很好的建议 。请在相应的 讨论论坛 上与作者和其他读者分享您对这篇文章的想法。(您也 可以单击本文顶部或底部的 讨论 访问论坛。)Swing 框架以事件侦听器的形式广泛利用了观察者模式(也称为发布-订阅模 式)。Swing 组件作为用户交互的目标,在用户与它们交互的时候触发事件;数 据模型类在数据发生变化时触发事件。用这种方式使用观察者,可以让控制器与 模型分离,让模型与视图分离,从而简化 GUI 应用程序的开发。“四人帮”的 设计模式 一书(参阅 参考资料)把观察者模式描述为:定义 对象之间的“一对多”关系,这样一个对象改变状态时,所有它的依赖项都会被 通知,并自动更新。观察者模式支持组件之间的松散耦合;组件可以保持它们的 状态同步,却不需要直接知道彼此的标识或内部情况,从而促进了组件的重用。AWT 和 Swing 组件(例如 JButton 或 JTable)使用观察者模式消除了 GUI 事件生成与它们在指定应用程序中的语义之间的耦合。类似地,Swing 的模型类 ,例如 TableModel 和 TreeModel,也使用观察者消除数据模型表示 与视图生 成之间的耦合,从而支持相同数据的多个独立的视图。Swing 定义了 Event 和 EventListener 对象层次结构;可以生成事件的组件,例如 JButton(可视组件 ) 或 TableModel(数据模型),提供了 addXxxListener() 和 removeXxxListener() 方法,用于侦听器的登记和取消登记。这些类负责决定什 么时候它们需要触发事件,什么时候确实触发事件,以及什么时候调用所有登记 的侦听器。为了支持侦听器,对象需要维护一个已登记的侦听器列表,提供侦听器登记 和取消登记的手段,并在适当的事件发生时调用每个侦听器。使用和支持侦听器 很容易(不仅仅在 GUI 应用程序中),但是在登记接口的两边(它们是支持侦 听器的组件和登记侦听器的组件)都应当避免一些缺陷。线程安全问题通常,调用侦听器的线程与登记侦听器的线程不同。要支持从不同线程登记 侦听器,那么不管用什么机制存储和管理活动侦听器列表,这个机制都必须是线 程安全的。Sun 的文档中的许多示例使用 Vector 保存侦听器列表,它解决了部 分问题,但是没有解决全部问题。在事件触发时,触发它的组件会考虑迭代侦听 器列表,并调用每个侦听器,这就带来了并发修改的风险,比如在侦听器列表迭 代期间,某个线程偶然想添加或删除一个侦听器。管理侦听器列表假设您使用 Vector<Listener> 保存侦听器列表。虽然 Vector 类是 线程安全的(意味着不需要进行额外的同步就可调用它的方法,没有破坏 Vector 数据结构的风险),但是集合的迭代中包含“检测然后执行”序列,如 果在迭代期间集合被修改,就有了失败的风险。假设迭代开始时列表中有三个侦 听器。在迭代 Vector 时,重复调用 size() 和 get() 方法,直到所有元素都 检索完,如清单 1 所示:清单 1. Vector 的不安全迭代
Vector<Listener> v;
for (int i=0; i<v.size(); i++)
v.get(i).eventHappened(event);但是,如果恰好就在最后一次调用 Vector.size() 之后,有人从列表中删除 了一个侦听器,会发生什么呢?现在,Vector.get() 将返回 null (这是对的 ,因为从上次检测 vector 的状态以来,它的状态已经变了),而在试图调用 eventHappened() 时,会抛出 NullPointerException。这是“检测然后执行” 序列的一个示例 —— 检测是否存在更多元素,如果存在,就取得下一元素 — — 但是在存在并发修改的情况下,检测之后状态可能已经变化。图 1 演示了这 个问题:图 1. 并发迭代和修改,造成意料之外的失败

这个问题的一个解决方案是在迭代期间持有对 Vector 的锁;另一个方案是 克隆 Vector 或调用它的 toArray() 方法,在每次发生事件时检索它的内容。 所有这两个方法都有性能上的问题:第一个的风险是在迭代期间,会把其他想访 问侦听器列表的线程锁在外面;第二个则要创建临时对象,而且每次事件发生时 都要拷贝列表。如果用迭代器(Iterator)去遍历侦听器列表,也会有同样的问题,只是表 现略有不同; iterator() 实现不抛出 NullPointerException,它在探测到迭 代开始之后集合发生修改时,会抛出 ConcurrentModificationException。同样 ,也可以通过在迭代期间锁定集合防止这个问题。java.util.concurrent 中的 CopyOnWriteArrayList 类,能够帮助防止这个 问题。它实现了 List,而且是线程安全的,但是它的迭代器不会抛出 ConcurrentModificationException,遍历期间也不要求额外的锁定。这种特性 组合是通过在每次列表修改时,在内部重新分配并拷贝列表内容而实现的,这样 ,遍历内容的线程不需要处理变化 —— 从它们的角度来说,列表的内容在遍历 期间保持不变。虽然这听起来可能没效率,但是请记住,在多数观察者情况下, 每个组件只有少量侦听器,遍历的数量远远超过插入和删除的数量。所以更快的 迭代可以补偿较慢的变化过程,并提供更好的并发性,因为多个线程可以同时迭 代列表。