听众应该能够删除听众吗?

Dan*_*ahn 16 java

许多类使用类似于以下的代码来激活侦听器.

private List<Listener> listeners = new ArrayList<Listener>();

public void fireListener() {
  for(Listener l : listeners) l.someMethod();
}
Run Code Online (Sandbox Code Playgroud)

这一点很好,直到侦听器尝试添加/删除侦听器.从列表内部进行的这种修改会导致a ConcurrentModificationException.我们应该处理这种情况还是应该修改听众无效?处理添加/删除侦听器的最佳方法是什么?

更新:
这是一个可能的解决方案.

public void fireListener() {
  for(Listener l : listeners.toArray(new Listener[listeners.size()])) {
    if(!listeners.contains(l)) continue;
    l.someMethod();
  }
}
Run Code Online (Sandbox Code Playgroud)

flo*_*flo 15

有三种情况:

  1. 您不希望在侦听器执行期间允许修改侦听器集合:在这种情况下,
    A ConcurrentModificationException是合适的.

  2. 您希望允许修改侦听器,但更改不会反映在当前运行中:
    您必须确保修改listeners不会对当前运行产生影响.一CopyOnWriteArrayList招即可.在使用它之前阅读API,有一些陷阱.
    另一个解决方案是在迭代之前复制列表.

  3. 您希望更改listeners反映在当前运行中:
    最棘手的情况.使用for-each循环和迭代器在这里不起作用(正如你已经注意到的那样;-)).你必须自己跟踪变化.
    一个可能的解决方案是将侦听器存储在一个ArrayList并使用标准for循环遍历该列表:

    for (int i =0; i < listeners.size(); i++) { 
        Listener l = listeners.get(i);
        if (l == null) 
            continue;
        l.handleEvent();
    }  
    
    Run Code Online (Sandbox Code Playgroud)

    删除侦听器会将元素在数组中的位置设置为null.新的侦听器被添加到最后,因此将在当前执行中运行.
    请注意,此解决方案只是一个示例,而不是线程安全!null有时需要一些维护来删除元素,以防止列表变得过大.

由您来决定需要什么.

我个人最喜欢的是第二个.它允许在执行时进行修改,但不会更改当前运行的行为,从而导致意外结果.


ccj*_*mne 9

我相信在触发时移除听众感觉就像代码味道,应该避免.


但是,尽管我能尽力回答你的问题:
在我看来,在浏览时添加/删除侦听器的最佳方法是使用支持它迭代器.

例如:

// Assuming the following:
final List<Listener> listeners = ...

final Iterator<Listener> i = listeners.iterator();
while (i.hasNext()) {
   // Must be called before you can call i.remove()
   final Listener listener = i.next(); 
   // Fire an event to the listener or whatever...

   i.remove(); // that's where the magic happens :)
}
Run Code Online (Sandbox Code Playgroud)

请注意,某些Iterators 不支持该Iterator#remove方法!

  • 实际上,从另一个对象的内部数据列表中"移除自己"有点困难,公平!请考虑我的答案的第一部分,这可能有点令人失望,但很可能是有用的,或@ biziclop的答案,以了解如何使用我的代码让听众"自我删除".干杯! (5认同)

biz*_*lop 5

我会说这是不是一个很好的主意,让听众添加/删除其他听众(或自己).它表明关注点分离不佳.毕竟,为什么听众应该对调用者有所了解?你会如何测试这种紧密耦合的系统?

有些事情,你可以做的反而是有你的事件处理方法(在听众)返回一个布尔值标志,指示听者不希望收到更多的事件.这使得事件调度程序负责执行删除操作,并且它涵盖了大多数需要从侦听器中修改侦听器列表的用例.

这种方法的主要区别是,听者只是说一些关于自己(即"我不想让更多的事件"),而不是依赖于调度程序的执行.这种解耦提高了可测试性,并且不会将任何一个类的内部暴露给另一个.

public interface FooListener {
    /**
     * @return False if listener doesn't want to receive further events.
     */
    public boolean handleEvent(FooEvent event);
}

public class Dispatcher {

    private final List<FooListener> listeners;

    public void dispatch(FooEvent event) {
      Iterator<FooListener> it = listeners.iterator();
      while (it.hasNext()) {
        if (!it.next().handleEvent(event))
          it.remove();
      }
    }
}
Run Code Online (Sandbox Code Playgroud)

更新:从听众中添加和删除其他听众稍微有点问题(而且应该掀起更响亮的警钟),但你可以遵循类似的模式:听众应该返回其他听众需要添加/删除的内容,信息和调度员应根据该信息采取行动.

但是在这种情况下,您会遇到很多边缘情况:

  • 当前事件会发生什么?
  • 是否应该发送给新添加的听众?
  • 是否应该发送给即将被删除的人?
  • 如果您尝试删除列表中较早的内容并且事件已发送给它,该怎么办?
  • 此外,如果侦听器X添加侦听器Y然后删除侦听器X,该怎么办?Y应该配合吗?

所有这些问题都来自于监听器模式本身以及它的基本假设,即列表中的所有监听器将彼此独立.处理这些案件的逻辑肯定应该放在调度员而不是听众中.

更新2:在我的例子中,我使用裸布尔来简洁,但在实际代码中我定义了一个双值枚举类型,以使契约更明确.