17

Many classes use code similar to the following to fire listeners.

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

public void fireListener() {
  for(Listener l : listeners) l.someMethod();
}

This works fine until the listener tries to add/remove a listener. This modification from inside the list causes a ConcurrentModificationException. Should we handle this case or should modifying listeners be invalid? What would be the best way to handle add/remove of listeners?

Update:
Here is one possible solution.

public void fireListener() {
  for(Listener l : listeners.toArray(new Listener[listeners.size()])) {
    if(!listeners.contains(l)) continue;
    l.someMethod();
  }
}
ccjmne
  • 9,333
  • 3
  • 47
  • 62
Dan Grahn
  • 9,044
  • 4
  • 37
  • 74
  • 1
    Please post a more detailed question with a larger code snippet to be clear . – Sainath S.R Oct 06 '15 at 14:33
  • Isn't `listeners` some sort of collection (like a list)? If that's the case, [we know to remove those with an `Iterator`.](http://stackoverflow.com/q/223918/1079354) – Makoto Oct 06 '15 at 14:36
  • Maybe I'm not answering the question, but generally using a FOR for removing elements usually ends with an exception. You should use an Iterator for removing while iterating. – spekdrum Oct 06 '15 at 14:38
  • It seems odd that your listener has access to the collection in which it is contained, the listener should be a distinct responsibility. If it needs to remove itself it should probably call whatever owns the collection and ask it to remove things on its behalf. – Paolo Oct 06 '15 at 14:39
  • 1
    @Paolo It could call `ParentClass#removeListener(Listener)` – Dan Grahn Oct 06 '15 at 14:42
  • That's fine and then the parent class needs to be responsible for synchronising that listeners aren't removed while it is also iterating through them. – Paolo Oct 06 '15 at 14:43
  • If the question is whether the listener should be able to remove itself it's mostly a matter of opinion (although there's complication with modifying the list while iterating). – skyking Oct 06 '15 at 14:47
  • 5
    I would suggest that a listener has no business whatsoever removing other listeners, but might want to remove itself. But it doesn't need to, it could just stop listening. It could then be re-activated later if needed. – NickJ Oct 06 '15 at 14:47
  • Alternative to `toArray` is using `CopyOnWriteArrayList`: http://docs.oracle.com/javase/7/docs/api/java/util/concurrent/CopyOnWriteArrayList.html – Kirill Rakhman Oct 06 '15 at 21:44

5 Answers5

17

There are three cases:

  1. You don't want to allow the modification of the listeners collection during listeners execution:
    A ConcurrentModificationException would be appropriate in this case.

  2. You want to allow modification of listeners, but the changes are not to be reflected in the current run:
    You have to make sure a modification of listeners does not have impact on the current run. A CopyOnWriteArrayList does the trick. Before using it read the API, there are some pitfalls.
    Another solution would be copying the list before iterating through it.

  3. You want changes to listeners reflect within the current run:
    The most tricky case. Using for-each loops and iterators won't work here (as you have noticed ;-)). You have to keep track of changes yourself.
    A possbile solution would be to store listeners in an ArrayList and go through that list using a standard for loop:

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

    Removing listeners would set the element at its place in the array to null. New listeners are added to the end and therefore will run in the current execution.
    Notice that this solution is just an example and not threadsafe! Maybe some maintainance is necessary to remove null elements sometimes to keep the list from growing too big.

It is up to you to decide what is needed.

My personal favourite would be the second one. It allows modifications while execution but does not change the current run's behaviour which can cause unexpected results.

John Kugelman
  • 349,597
  • 67
  • 533
  • 578
flo
  • 9,713
  • 6
  • 25
  • 41
  • ad 3) iterating from last to first element would alleviate the need for `null` handling – mucaho Oct 06 '15 at 20:20
  • 1
    Agreed: Listeners are usually added/removed rarely, but may be iterated over frequently, so this fits nicely into the idiomatic use-cases of a `CopyOnWriteArrayList`. – Marco13 Oct 06 '15 at 21:48
9

The right way to handle the addition/removal of listeners while browsing them would be to use an iterator that supports this.

For example:

// 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
   i.notify();

   if (/* true iff listener wants to be deregistered */) {
       i.remove(); // that's where the magic happens :)
   }
}

NB: Please note that certain Iterators do not support the Iterator#remove method!

ccjmne
  • 9,333
  • 3
  • 47
  • 62
  • 1
    This isn't very useful since the listener isn't removing itself here, which is what the question is about. – Dan Grahn Oct 06 '15 at 14:41
  • 5
    It's somewhat hard to actually "remove oneself" from another object's internal data lists, to be fair! Please consider the first part of my answer, which is probably a tad disappointing but very likely to be useful, or @biziclop's answer to understand how you can use my code for a listener to "remove itself". Cheers! – ccjmne Oct 06 '15 at 14:44
  • "and should be avoided" - What if, whatever the listener is listening for, the proper response to doing it would naturally involve removing that listener? E.g. listening for some sort of completion event, then when that is done, the listener gets removed? – The_Sympathizer Jul 25 '20 at 07:26
5

I would say it isn't a very good idea to allow listeners to add/remove other listeners (or themselves). It indicates a poor separation of concerns. After all, why should the listener know anything about the caller? How would you test such a tightly coupled system?

Something you can do instead is have your event handling method (in the listener) return a boolean flag that indicates that the listener doesn't want to receive more events. That makes it the responsibility of the event dispatcher to do the removal, and it covers most use cases where you need to modify the list of listeners from within a listener.

The major difference in this approach is that the listener simply says something about itself (i.e. "I don't want more events") rather than being tied to the implementation of the dispatcher. This decoupling improves testability and doesn't expose the internals of either class to the other.

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();
      }
    }
}

Update: Adding and removing other listeners from within a listener is slightly more problematic (and should set off even louder alarm bells) but you can follow a similar pattern: the listener should return information on what other listeners need to be added/removed and the dispatcher should act on that information.

However in this scenario you get quite a few edge cases:

  • What should happen with the current event?
  • Should it be dispatched to the newly added listeners?
  • Should it be dispatched to the ones about to be removed?
  • What if you're trying to remove something that is earlier in the list and the event has already been sent to it?
  • Also, what if listener X adds listener Y and then listener X is removed? Should Y go with it?

All these problems come from the listener pattern itself and the basic assumption of it that all the listeners in the list will be independent of each other. And the logic to handle these cases should definitely go in the dispatcher and not in the listener.

Update 2: In my example I used a naked boolean for brevity but in real code I'd define a two-valued enum type to make the contract more explicit.

biziclop
  • 48,926
  • 12
  • 77
  • 104
  • The listener may not know directly, but what if it notifies something else that does - e.g. it's listening for some kind of completion event, and upon that completion it now has to be removed? – The_Sympathizer Jul 25 '20 at 07:26
2

You may first make a copy of listeners Collection to avoid possible ConcurrentModificationException:

public void fireListener() {
  Listener[] ll = listeners.toArray(new Listener[listeners.size()]);
  for(Listener l : ll) l.someMethod();
}
Tagir Valeev
  • 97,161
  • 19
  • 222
  • 334
-1

You can't add/remove to a list/map that's being used. In C# there's a nice little class called ConcurrentList/ConcurrentDictionary.

In Java this is possible, here a nice little link for you:

https://docs.oracle.com/javase/tutorial/essential/concurrency/collections.html

Since a collection is used within a loop, the collection is used for a certain time. If you add/remove in the function called within the loop, you'll get an error as you're not allowed to without Concurrent.

Joshua Bakker
  • 2,288
  • 3
  • 30
  • 63
  • 3
    Why should I care about C# here? :) – Maroun Oct 06 '15 at 14:37
  • 1
    This doesn't answer the question. – Dan Grahn Oct 06 '15 at 14:39
  • I thought concurrent collections weren't in Java, but appearantly they are. I forgot to remove the C# part as it's not needed for the answer. And @screenmutt then I misunderstood the question. – Joshua Bakker Oct 06 '15 at 14:39
  • Thread synchronization is insufficient for iteration. [`ConcurrentModificationException`](http://docs.oracle.com/javase/7/docs/api/java/util/ConcurrentModificationException.html) can indicate that you tried to perform an operation that would **invalidate the `Iterator`**. This happens even if the `List` is only created and used within a single method. – jpmc26 Oct 06 '15 at 23:05