Java理论与实践:做个好的(事件)侦听器
副标题#e#
调查者模式在 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。同样 ,也可以通过在迭代期间锁定荟萃防备这个问题。
#p#分页标题#e#
java.util.concurrent 中的 CopyOnWriteArrayList 类,可以或许辅佐防备这个 问题。它实现了 List,并且是线程安详的,可是它的迭代器不会抛出 ConcurrentModificationException,遍历期间也不要求特另外锁定。这种特性 组合是通过在每次列表修改时,在内部从头分派并拷贝列表内容而实现的,这样 ,遍历内容的线程不需要处理惩罚变革 —— 从它们的角度来说,列表的内容在遍历 期间保持稳定。固然这听起来大概没效率,可是请记着,在大都调查者环境下, 每个组件只有少量侦听器,遍历的数量远远高出插入和删除的数量。所以更快的 迭代可以赔偿较慢的变革进程,并提供更好的并发性,因为多个线程可以同时迭 代列表。
#p#副标题#e#
初始化的安详风险
从侦听器的结构函数中挂号它很诱惑人,可是这是一个该当制止的诱惑。它 仅会造成“失效侦听器(lapsed listener)的问题(我稍后接头它),并且还 会造成多个线程安详问题。清单 2 显示了一个看起来没什么害处的同时结构和 挂号侦听器的诡计。问题是:它造成到工具的“this”引用在工具完全结构完成 之前转义。固然看起来没什么害处,因为挂号是结构函数做的最后一件事,可是 看到的对象是有欺骗性的:
清单 2. 事件侦听器答允“this”引用转义,造成问题public class EventListener {
public EventListener(EventSource eventSource) {
// do our initialization
...
// register ourselves with the event source
eventSource.registerListener(this);
}
public onEvent(Event e) {
// handle the event
}
}
在担任事件侦听器的时候,会呈现这种要领的一个风险:这时,子类结构函 数做的任何事情都是在 EventListener 结构函数运行之后举办的,也就是在 EventListener 宣布之后,所以会造成争用环境。在某些不幸的时候,清单 3 中的 onEvent 要了解在列表字段还没初始化之前就被挪用,从而在打消 final 字段的引用时,会生成很是让人狐疑的 NullPointerException 异常:
清单 3. 担任清单 2 的 EventListener 类造成的问题public class RecordingEventListener extends EventListener {
private final ArrayList<Event> list;
public RecordingEventListener(EventSource eventSource) {
super(eventSource);
list = Collections.synchronizedList(new ArrayList<Event> ());
}
public onEvent(Event e) {
list.add(e);
super.onEvent(e);
}
}
纵然侦听器类是 final 的,不能派生子类,也不应当答允“this”引用在构 造函数中转义 —— 这样做会危害 Java 内存模子的某些安详担保。假如“this ”这个词不会呈此刻措施中,就可让“this”引用转义;宣布一个非静态内部类 实例可以到达沟通的结果,因为内部类持有对它困绕的工具的“this”引用的引 用。偶尔地答允“this”引用转义的最常见原因,就是挂号侦听器,如清单 4 所示。事件侦听器不应当在结构函数中挂号!
清单 4. 通过宣布内部类实例,显式地答允“this”引用转义public class EventListener2 {
public EventListener2(EventSource eventSource) {
eventSource.registerListener(
new EventListener() {
public void onEvent(Event e) {
eventReceived(e);
}
});
}
public void eventReceived(Event e) {
}
}
侦听器线程安详
利用侦听器造成的第三个线程安详问题来自这个事实:侦听器大概想会见应 用措施数据,而挪用侦听器的线程凡是不直接在应用措施的节制之下。假如在 JButton 或其他 Swing 组件上挂号侦听器,那么会从 EDT 挪用该侦听器。侦听 器的代码可以从 EDT 安详地挪用 Swing 组件上的要领,可是假如工具自己不是 线程安详的,那么从侦听器会见应用措施工具会给应用措施增加新的线程安详需 求。
Swing 组件生成的事件是用户交互的功效,可是 Swing 模子类是在 fireXxxEvent() 要领被挪用的时候生成事件。这些要领又会在挪用它们的线程 中挪用侦听器。因为 Swing 模子类不是线程安详的,并且假设被限制在 EDT 内 ,所以对 fireXxxEvent() 的任何挪用也都该当从 EDT 执行。假如想从别的的 线程触发事件,那么该当用 Swing 的 invokeLater() 成果让要领转而在 EDT 内挪用。一般来说,要留意挪用事件侦听器的线程,还要担保它们涉及的任何对 象可能是线程安详的,可能在会见它们的处所,受到适当的同步(可能是 Swing 模子类的线程约束)的掩护。
失效侦听器
#p#分页标题#e#
不管什么时候利用调查者模式,都耦合着两个独立组件 —— 调查者和被观 察者,它们凡是有差异的生命周期。挂号侦听器的效果之一就是:它在被调核对 象和侦听器之间成立起很强的引用干系,这种干系防备侦听器(以及它引用的对 象)被垃圾收集,直到侦听器打消挂号为止。在很多环境下,侦听器的生命周期 至少要和被调查的组件一样长 —— 很多侦听器会在整个应用措施期间都存在。 可是在某些环境下,该当短期存在的侦听器最后酿成了永久的,它们这种无意识 的拖延的证据就是应用措施机能变慢、高于必须的内存利用。
“失效侦听器”的问题可以由设计级别上的不小心造成:没有恰内地思量包 含的工具的寿命,可能由于松懈的编码。侦听器挂号和打消挂号该当结对举办。 可是纵然这么做,也必需担保是在正确的时间执行打消挂号。清单 5 显示了会 造成失效侦听器的编码习惯的示例。它在组件上挂号侦听器,执行某些行动,然 后打消挂号侦听器:
清单 5. 有造成失效侦听器风险的代码 public void processFile (String filename) throws IOException {
cancelButton.registerListener(this);
// open file, read it, process it
// might throw IOException
cancelButton.unregisterListener(this);
}
清单 5 的问题是:假如文件处理惩罚代码抛出了 IOException —— 这是很有可 能的 —— 那么侦听器就永远不会打消挂号,这就意味着它永远不会被垃圾收集 。打消挂号的操纵该当在 finally 块中举办,这样,processFile() 要领的所 有出口城市执行它。
有时推荐的一个处理惩罚失效侦听器的要领是利用弱引用。固然这种要领可行, 可是实现起来很贫苦。要让它事情,需要找到别的一个工具,它的生命周期刚好 是侦听器的生命周期,并布置它持有对侦听器的强引用,这可不是件容易的事。
别的一项可以用来找到埋没失效侦听器的技能是:防备指定侦听器工具在指 定事件源上挂号两次。这种环境凡是是 bug 的迹象 —— 侦听器挂号了,可是 没有打消挂号,然后再次挂号。不消检测问题,就能缓解这个问题的影响的一种 方法是:利用 Set 取代 List 来存储侦听器;可能也可以检测 List,在挂号侦 听器之前查抄是否已经挂号了,假如已经挂号,就抛出异常(或记录错误),这 样就可以汇集编码错误的证据,并采纳动作。
其他侦听器问题
在编写侦听器时,该当一直留意它们将要执行的情况。不只要留意线程安详 问题,还需要记着:侦听器也可以用其他方法为它的挪用者把工作搞糟。侦听器 不应 做的一件事是:阻塞相当长一段时间(长得可以感受获得);挪用它的执 行上下文很大概但愿迅速返回节制。假如侦听器要执行一个大概较量费时的操纵 ,譬喻处理惩罚大型文本,可能要做的事情大概阻塞,譬喻执行 socket IO,那么侦 听器该当把这些操纵布置在另一个线程中举办,这样它就可以迅速返回它的挪用 者。
对付不小心的事件源,侦听器会造成贫苦的另一个方法是:抛出未检测的异 常。固然大大都时候,我们不会存心抛出未检测异常,可是确实有些时候会产生 这种环境。假如利用清单 1 的方法挪用侦听器,列表中的第二个侦听器就会抛 出未检测异常,那么不只后续的侦听器得不到挪用(大概造成应用措施处在纷歧 致的状态),并且有大概把执行它的线程粉碎掉,从而造成局部应用措施失败。
在挪用未知代码(侦听器就是这样的代码)时,审慎的方法是在 try-catch 块中执行它,这样,行为有误的侦听器不会造成更多不须要的粉碎。对付抛出未 检测异常的侦听器,您大概想自动对它打消挂号,究竟,抛出未检测异常就证明 侦听器坏掉了。(您大概还想记录这个错误可能提醒用户留意,好让用户可以或许知 道为什么措施遏制像期望的那样继承事情。)清单 6 显示了这种方法的一个示 例,它在迭代轮回内部嵌套了 try-catch 块:
清单 6. 结实的侦听器挪用List<Listener> list;
for (Iterator<Listener> i=list.iterator; i.hasNext(); ) {
Listener l = i.next();
try {
l.eventHappened(event);
}
catch (RuntimeException e) {
log("Unexpected exception in listener", e);
i.remove();
}
}
竣事语
#p#分页标题#e#
调查者模式对付建设松散耦合的组件、勉励组件重用很是有用,可是它有一 些风险,侦听器的编写者和组件的编写者都该当留意。在挂号侦听器时,该当一 直留意侦听器的生命周期。假如侦听器的寿命该当比应用措施的短,那么请确保 打消它的挂号,这样它就可以被垃圾收集。在编写侦听器和组件时,请留意它包 含的线程安详性问题。侦听器涉及的任何工具,都该当是线程安详的,可能是受 线程约束的工具(譬喻 Swing 模子),侦听器该当确定本身正在正确的线程中 执行。