Java中适合事件监听器的集合类

14

相关内容: Java中是否有“LinkedConcurrentHashMap”数据结构?


我正在寻找一个用于保存事件监听器引用的集合类。

我的理想方案是,这个集合具有以下属性(按优先级排序):

  1. 保持插入顺序。早期的侦听器可能会取消事件,从而防止它被传递到后来添加的侦听器。如果使用像HashSet这样的类,其迭代器可能以错误的顺序返回元素,这将导致出错。
  2. 使用WeakReference以使监听器列表不会阻止监听器被垃圾回收。
  3. 集合是Set,因此重复项会自动删除。
  4. Iterator是线程安全快照,不受新侦听器添加的影响。也允许在多个线程上传递事件。(这不是必需的 - 我可以遍历集合的副本。)

我知道有一些类满足其中的一些但不是全部标准。例如:

  • java.util.LinkedHashSet (#1 和 #3)
  • java.util.WeakHashMap,由 Collections.newSetFromMap 包装(#2 和 #3)
  • javax.swing.event.EventListenerList (需要一些额外的同步)(#1 和 #4)
  • java.util.concurrent.CopyOnWriteArraySet (#1, #3和#4)

但没有同时满足标准#1和#2的类。这样的类是否存在于某个库中?


监听器对象实现 equals 方法真的很典型吗? - Kevin Bourrillion
4
还有,谁负责保留每个侦听器的强引用?如果我编写一个侦听器来记录各种事件,并注册它,当它突然在几分钟后消失时,我肯定会感到惊讶! - Kevin Bourrillion
equals方法的问题在于:{Object o = new Object(); WeakReference r1 = new WeakReference(o), r2 = new WeakReference(o); return r1.equals(r2);} 返回 false - finnw
这可能是匿名监听器的问题,但我的监听器是 ListModel,因此它们通常会从 GUI 中被强引用。 - finnw
3
关于第二点:我认为你不应该依赖垃圾收集器来清理你的混乱。如果你在某个地方注册了一个监听器,你应该自己撤销它,而不是交叉双手希望它会在某个时刻自动消失。通常,当触发监听器时,它会执行一些操作,因为你无法知道垃圾收集何时开始工作,所以你会得到非常不确定的行为... - FRotthowe
5个回答

7
我首先要说的是,您有一些不合理的要求。您正在寻找一个移除重复项并支持弱引用的集合,这表明监听器可能会在不确定的时间出现和消失。然而,您希望保持插入顺序,并允许一个监听器取消所有后续通知。对我来说,这听起来像是一个难以找到错误的配方,我强烈建议重新考虑。
话虽如此,您有一个几乎推动解决方案的要求:您不想从正常迭代器中出现ConcurrentModificationException。这意味着您必须复制原始列表。在此过程中,您可以检查并删除空引用:
// the master list
List<WeakReference<MyListener>> _list = new ArrayList<WeakReference<MyListener>>();

// inside your send-notification method
List<MyListener> toNotify = new ArrayList<MyListener>(_list.size());
Iterator<WeakReference<MyListener>> itx = _list.iterator();
while (itx.hasNext())
{
    WeakReference<MyListener> ref = itx.next();
    MyListener lsnr = ref.get();
    if (lsnr != null)
        toNotify.add(lsnr);
    else
        itx.remove();
}

// now iterate "toNotify" and invoke the listeners

您现在可能有些恐慌,会说“一个列表!那是一种线性数据结构!我不能用它,插入是O(N)!”
好吧,其实您可以使用。我不知道您计划拥有多少个监听器。但只要您小于100(更可能小于100,000),线性搜索插入和删除的成本就不会有太大影响。
从编码角度来看,更有趣的是如何处理弱引用。您会注意到,我在测试引用对象是否为空之前,将其显式解除引用,并存储在变量中。这是处理引用对象时至关重要的代码:虽然极不可能在两次调用get()之间收集引用对象,但这是有可能的。
这就带我们来到了 WeakReference 本身。您需要创建自己的子类,覆盖 equals() 和 hashCode() 方法以委托给其引用对象。我认为我有一个这样的类放在那里,但显然没有,所以我会留给您来实现。

7
您可以使用WeakListeners(请参见http://bits.netbeans.org/dev/javadoc/org-openide-util/org/openide/util/WeakListeners.html)和CopyOnWriteArraySet。
实施一个remove(ListenerType listener)方法在您的事件源中。
在您的register(SomeListener listener)方法中,将弱监听器添加到集合中: listenerCollection.put((ListenerType)WeakListeners.create ( ListenerType.class, listener, this)); 当真正的监听器从内存中移除时,弱监听器将被通知,并取消注册自己。(这就是为什么它需要对源(this)的引用进行注册。)取消注册是通过反射调用源的remove方法完成的。

1

使用Set是与监听器一起使用的适当集合。

如果您依赖于侦听器的插入顺序,则您的设计已经出错了。它忽略了监听器与其他监听器隔离和独立的点。使用Set而不是List。

如果您依赖于WeakReferences,则您的设计已经出错了。在添加监听器的同一对象中删除它。这种对称性支持可读性和可维护性。仅使用弱引用来解决忘记取消订阅带来的编程错误只会隐藏问题。

如果您将监听器的集合提供给观察对象以外的其他对象,则您的设计已经出错了。保持Set私有以支持封装。

如果您覆盖监听器的equals和hashcode方法,则您的设计已经出错了。它隐藏了不必要的函数调用问题。相反,应该避免不必要的调用。毕竟,覆盖监听器的equals和hashcode方法并不是必需的。

在多线程环境中,在添加、删除或迭代“listeners”资源时在其上放置监视器。您可以在迭代之前创建一个防御性副本,以避免ConcurrentModificationException异常。然后迭代就不必被同步,但复制操作应该被同步。

任何其他要求都必须被适应或改写以符合这些声明。任何其他做法都会导致无法维护的代码,由于缺乏隔离、独立性、封装和清晰度而导致内存泄漏。


0
你可以将每个监听器引用包装在一个 WeakReference 中,然后使用 CopyOnWriteArraySet

有两个问题:1. 垃圾回收引用将永远留在集合中(WeakHashMap 会自动清除它们),2. WeakReference 没有覆盖 equals() 方法,因此您可能会在集合中得到对同一侦听器的多个引用,但无法确定它们是否重复。 - finnw
True。也许你可以通过扩展CopyOnWriteArraySet来检查引用而不是WeakReference,当集合变异并手动清除过时的引用来克服这个问题。我认为标准集合中没有任何东西可以完全满足你的需求。 - Rob H

0
你可以扩展WeakReference类并重写equals和hashcode方法,然后就可以在LinkedHashSet中使用它们了。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接