许多类使用类似于以下的代码来激活侦听器.
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
有三种情况:
您不希望在侦听器执行期间允许修改侦听器集合:在这种情况下,
A ConcurrentModificationException是合适的.
您希望允许修改侦听器,但更改不会反映在当前运行中:
您必须确保修改listeners不会对当前运行产生影响.一CopyOnWriteArrayList招即可.在使用它之前阅读API,有一些陷阱.
另一个解决方案是在迭代之前复制列表.
您希望更改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有时需要一些维护来删除元素,以防止列表变得过大.
由您来决定需要什么.
我个人最喜欢的是第二个.它允许在执行时进行修改,但不会更改当前运行的行为,从而导致意外结果.
我相信在触发时移除听众感觉就像代码味道,应该避免.
但是,尽管我能尽力回答你的问题:
在我看来,在浏览时添加/删除侦听器的最佳方法是使用支持它的迭代器.
例如:
// 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方法!
我会说这是不是一个很好的主意,让听众添加/删除其他听众(或自己).它表明关注点分离不佳.毕竟,为什么听众应该对调用者有所了解?你会如何测试这种紧密耦合的系统?
有些事情,你可以做的反而是有你的事件处理方法(在听众)返回一个布尔值标志,指示听者不希望收到更多的事件.这使得事件调度程序负责执行删除操作,并且它涵盖了大多数需要从侦听器中修改侦听器列表的用例.
这种方法的主要区别是,听者只是说一些关于自己(即"我不想让更多的事件"),而不是依赖于调度程序的执行.这种解耦提高了可测试性,并且不会将任何一个类的内部暴露给另一个.
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)
更新:从听众中添加和删除其他听众稍微有点问题(而且应该掀起更响亮的警钟),但你可以遵循类似的模式:听众应该返回其他听众需要添加/删除的内容,信息和调度员应根据该信息采取行动.
但是在这种情况下,您会遇到很多边缘情况:
所有这些问题都来自于监听器模式本身以及它的基本假设,即列表中的所有监听器将彼此独立.处理这些案件的逻辑肯定应该放在调度员而不是听众中.
更新2:在我的例子中,我使用裸布尔来简洁,但在实际代码中我定义了一个双值枚举类型,以使契约更明确.