Должны ли слушатели удалять слушателей?
Многие классы используют код, похожий на следующий, для запуска прослушивателей. (Примечание: это общий фрагмент)
private List<Listener> listeners = new ArrayList<Listener>();
public void fireListener() {
for(Listener l : listeners) l.someMethod();
}
Это работает нормально, пока слушатель не попытается добавить/удалить прослушиватель. Эта модификация из списка вызывает ConcurrentModificationException
. Должны ли мы обращаться с этим делом или должны ли изменения прослушивателей быть недействительными? Каким будет лучший способ обработки добавления/удаления слушателей?
Обновление
Вот одно из возможных решений.
public void fireListener() {
for(Listener l : listeners.toArray(new Listener[listeners.size()])) {
if(!listeners.contains(l)) continue;
l.someMethod();
}
}
Ответы
Ответ 1
Есть три случая:
-
Вы не хотите разрешать изменение коллекции слушателей во время исполнения слушателей:
A ConcurrentModificationException
было бы подходящим в этом случае.
-
Вы хотите разрешить модификацию слушателей, но изменения не должны отражаться в текущем запуске:
Вы должны убедиться, что изменение listeners
не влияет на текущий прогон. A CopyOnWriteArrayList
делает трюк. Прежде чем использовать его, прочитайте API, есть некоторые подводные камни.
Другим решением будет копирование списка перед его повторением.
-
Вы хотите, чтобы изменения в listeners
отражались в текущем запуске:
Самый сложный случай. Использование для каждого цикла и итераторов здесь не будет работать (как вы заметили;-)). Вы должны сами отслеживать изменения.
Допустимым решением было бы сохранить слушателей в ArrayList
и пройти через этот список, используя стандартный цикл:
for (int i =0; i < listeners.size(); i++) {
Listener l = listeners.get(i);
if (l == null)
continue;
l.handleEvent();
}
Удаление прослушивателей установит элемент на своем месте в массиве равным null.
Новые слушатели добавляются в конец и, следовательно, будут выполняться в текущем исполнении.
Обратите внимание, что это решение является всего лишь примером, а не потокобезопасным!
Возможно, некоторое обслуживание необходимо для удаления элементов null
, иногда для того, чтобы список стал слишком большим.
Вам решать, что нужно.
Мой личный фаворит был бы вторым. Он допускает модификации во время выполнения, но не изменяет текущее поведение запуска, которое может вызвать неожиданные результаты.
Ответ 2
Я считаю, что удаление слушателей при стрельбе их чувствует, как запах кода, и его следует избегать.
Однако, чтобы ответить на ваш вопрос, я могу:
На мой взгляд, лучший способ справиться с добавлением/удалением слушателей при их просмотре - использовать итератор, который поддерживает этот.
Например:
// 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 where the magic happens :)
}
Обратите внимание, что определенные Iterators
не поддерживают метод Iterator#remove
!
Ответ 3
Я бы сказал, что не очень хорошая идея позволить слушателям добавлять/удалять других слушателей (или самих себя). Это указывает на плохое разделение проблем. В конце концов, почему слушатель ничего не знает о вызывающем? Как бы вы протестировали такую тесно связанную систему?
Вместо этого вы можете использовать метод обработки событий (в слушателе), возвращающий логический флаг, который указывает, что слушатель не хочет получать больше событий. Это делает диспетчер событий ответственным за удаление, и он охватывает большинство случаев, когда вам нужно изменить список слушателей внутри слушателя.
Основное отличие в этом подходе заключается в том, что слушатель просто говорит что-то о себе (то есть "Я не хочу больше событий" ), а не привязан к реализации диспетчера. Эта развязка улучшает тестируемость и не подвергает внутренности любого класса другому.
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();
}
}
}
Обновление: Добавление и удаление других слушателей внутри слушателя несколько проблематично (и должно вызывать еще более громкие звуковые сигналы), но вы можете следовать аналогичной схеме: слушатель должен вернуть информацию о том, что другие слушатели должны быть добавлены/удалены, и диспетчер должен действовать по этой информации.
Однако в этом случае вы получаете довольно много случаев:
- Что должно произойти с текущим событием?
- Должен ли он отправляться недавно добавленным слушателям?
- Должен ли он отправляться в те, которые должны быть удалены?
- Что делать, если вы пытаетесь удалить что-то, что было ранее в списке, и событие уже отправлено ему?
- Кроме того, что, если слушатель X добавляет слушателя Y, а затем прослушиватель X удаляется? Должен ли я пойти с ним?
Все эти проблемы исходят из самого шаблона слушателя и основного предположения, что все слушатели в списке будут независимы друг от друга. И логика для обработки этих случаев обязательно должна идти в диспетчере, а не в слушателе.
Обновление 2: В моем примере я использовал голый булев для краткости, но в реальном коде я бы определил двузначный тип перечисления, чтобы сделать контракт более явным.
Ответ 4
Вы можете сначала сделать копию коллекции listeners
, чтобы избежать возможного ConcurrentModificationException
:
public void fireListener() {
Listener[] ll = listeners.toArray(new Listener[listeners.size()]);
for(Listener l : ll) l.someMethod();
}
Ответ 5
Вы не можете добавить/удалить к используемому списку/карте. В С# есть хороший небольшой класс под названием ConcurrentList/ConcurrentDictionary.
В Java это возможно, вот вам небольшая ссылка для вас:
https://docs.oracle.com/javase/tutorial/essential/concurrency/collections.html
Так как коллекция используется в цикле, коллекция используется в течение определенного времени. Если вы добавите/удалите функцию, вызванную в цикле, вы получите сообщение об ошибке, поскольку вам не разрешено без Concurrent.