14

Related: Does java have a "LinkedConcurrentHashMap" data structure?


I am looking for a collection class to hold references to event listeners.

Ideally I would like the collection to have the following properties (in order of priority):

  1. Maintains insertion order. The earlier listeners may cancel the event, preventing it from being delivered to listeners added later. This will break if using a class such as HashSet whose iterator may return elements in the wrong order.
  2. Uses WeakReferences so that the listener list does not prevent the listeners from being garbage-collected.
  3. The collection is a Set, so duplicates are automatically removed.
  4. The Iterator is a thread-safe snapshot of the collection, unaffected by the addition of new listeners. Also allows events to be delivered on multiple threads. (This is not essential - I could iterate over a clone of the set instead.)

I am aware of some classes that satisfy some but not all of these criteria. Examples:

  • java.util.LinkedHashSet (#1 and #3)
  • java.util.WeakHashMap, wrapped by Collections.newSetFromMap (#2 and #3)
  • javax.swing.event.EventListenerList (needs some extra synchronization) (#1 and #4)
  • java.util.concurrent.CopyOnWriteArraySet (#1, #3 and #4)

But nothing with both #1 and #2. Does class like this exist in a library somewhere?

Community
  • 1
  • 1
finnw
  • 47,861
  • 24
  • 143
  • 221
  • Is it really typical for listener objects to implement `equals`? – Kevin Bourrillion Jan 15 '10 at 21:19
  • 4
    Also, whose job is it to retain a strong reference to each listener? If I wrote a listener to log various events, and registered it, I'd sure be surprised when it suddenly disappeared a few minutes later! – Kevin Bourrillion Jan 15 '10 at 21:21
  • No the problem with equals is: `{Object o = new Object(); WeakReference r1 = new WeakReference(o), r2 = new WeakReference(o); return r1.equals(r2);}` returns `false` – finnw Jan 15 '10 at 23:11
  • This would be a problem with anonymous listeners, but my listeners are `ListModel` s so they will normally be strongly referenced from the GUI. – finnw Jan 15 '10 at 23:13
  • 3
    Regarding #2: I think you shouldn't rely on the garbage collection to clean your mess up. If you register a listener somewhere, you should deregister it yourself and not cross your fingers and hope that it will disappear automatically at some point in time. Normally, a listener *does* something when it's triggered, and because you cannot know when the garbage collection kicks in, you'll get very indeterministic behaviour... – FRotthowe Jan 18 '10 at 13:53

5 Answers5

7

I'm going to start by saying that you have a couple of requirements that don't make sense together. You're looking for a collection that removes duplicates and supports weak references, which indicates to me that listeners may appear and disappear at indeterminate times. Yet you want to maintain insertion order, and allow one listener to cancel all subsequent notifications. To me, this sounds like a recipe for hard-to-find bugs, and I strongly suggest rethinking it.

That said, you have one requirement that pretty much drives the solution: you don't want the ConcurrentModificationException that could come from a normal iterator. Which means that you're going to have to copy the original list. Along the way, you can check and remove the empty references:

// 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

You're probably freaking out now, saying "a List! that's a linear data structure! I can't use that, insertion is O(N)!"

Well, yes you can. I don't know how many listeners you're planning to have. But as long as you're < 100 (and more likely < 100,000), the cost of a linear search for insert and remove isn't going to matter.

Far more interesting from a coding perspective is how you deal with the weak reference. You'll note that I explicitly dereference it, into a variable, before testing the referent for null. This is critically important code when dealing with reference objects: although it's extremely unlikely that the referent will be collected between two calls to get(), it's possible.

Which brings me to the WeakReference itself. You'll need to create your own subclass that overrides the equals() and hashCode() methods to delegate to its referent. I thought I had just such a class lying around, but apparently not, so will leave it for you to implement.

kdgregory
  • 38,754
  • 10
  • 77
  • 102
7

You could use WeakListeners (see http://bits.netbeans.org/dev/javadoc/org-openide-util/org/openide/util/WeakListeners.html) and CopyOnWriteArraySet.

  1. Implement a remove(ListenerType listener) method in your event source.
  2. In your register(SomeListener listener) method, add a WeakListener to the collection instead:

    listenerCollection.put((ListenerType)WeakListeners.create ( ListenerType.class, listener, this));

When the real listener is removed from memory, the weak listener will be notified, and it will unregister itself. (This is why it needs the reference to the source (this) for the registration.) The unregistration is done using reflection by calling the method remove of the source.

Oleg Ryaboy
  • 3,079
  • 1
  • 17
  • 13
1

A Set is the proper collection to use with listeners.

If you rely on the insertion order of listeners your design is broken. It misses the point of listeners to be ISOLATED and INDEPENDENT from other listeners. Use Sets instead of Lists.

If you rely on WeakReferences your design is broken. Remove listeners in the same object where you added it. This SYMMETRY supports READABILITY and MAINTAINABILITY. To resolve programming errors of forgotton unsubcriptions of listeners with weak references only hides the problem.

If you provide your collection of listeners to other objects than to your observed object then your design is broken. Keep the Set private to support ENCAPSULATION.

If you override equals and hashcode of your listeners your design is broken. It hides the problem of unneccessary function calls. Prevent unneccessary calls instead. After all overiding equals and hashcode of listeners is not neccessary.

In MULTITHREADING environments put an MONITOR on the resource "listeners" while adding, removing or iterating over it. You may create a DEFENSIVE COPY before you iterate to avoid a ConcurrentModificationException. Then the iteration does not have to be SYNCHRONIZED, but the copy action should.

Any other requirement has to be adapted or reformulated to match these statements. Any other practise will lead to unmaintainable code, memory leaks because of lacking isolation, independency, encapsulation and clarity.

oopexpert
  • 767
  • 7
  • 12
0

You could wrap each listener reference in a WeakReference and then use CopyOnWriteArraySet.

Rob H
  • 14,502
  • 8
  • 42
  • 45
  • Two problems with that: 1. garbage-collected references will remain in the set forever (WeakHashMap automatically expunges them) and 2. `WeakReference` does not override `equals()`, so you can end up with multiple references to the same listener in the set but you cannot tell that they are duplicates. – finnw Jan 15 '10 at 15:55
  • True. Perhaps you could overcome this, though, by extending CopyOnWriteArraySet to examine the referent rather than the WeakReference when the collection mutates and manually expunge obsolete references. I don't believe anything in the standard Collections offers everything you want out-of-the-box. – Rob H Jan 15 '10 at 16:28
0

You could extend WeakReference to override equals and hashcode, then you can use them in a LinkedHashSet.

Damien B
  • 1,992
  • 15
  • 19