There are many good resources on using events and implementing event listeners. Unfortunately, there are nearly as many resources that incorrectly or inefficiently implement event generators. These implementations introduce subtle nondeterministic errors, execute slowly, and/or fill the virtual machine with unnecessary garbage. The goal of this article is to address this problem once and for all; as a pattern and code generator for JDK’s before 1.5, and as a set of generic types in JDK 1.5 and later.
Most Java programmers learn how to Listen early on, usually thanks to the Swing classes. The elements are as follows:
But here's the rub. These things are often not correctly accounted for:
The humble truth is, most of the generators in your application are not being listened to by anything, and most of the rest have one listener. For example, of the 18 events generated by a JButton, how many do you actually pay attention to?
Correct is more important than efficient. Below is a typical implementation of a correct, but inefficient, generator:
interface Event { /* whatever you need */ } interface Listener { void receive(Generator from, Event event); } interface Generator { void addListener(Listener listener); void removeListener(Listener listener); } class DefaultGenerator implements Generator { private ArrayList listeners = new ArrayList(); private Object lock = new Object(); public void addListener(Listener listener) { synchronized(lock) { if (listener != null && ! listeners.contains(listener)) { listeners.add(listener); } } } public void removeListener(Listener listener) { synchronized(lock) { listeners.remove(listener); } } protected ArrayList cloneListeners() { synchronized(lock) { return (ArrayList) listeners.clone(); } } protected void send(Event event) { ArrayList tmp = cloneListeners(); for (Iterator i = tmp.iterator(); i.hasNext(); ) { ((Listener) i.next()).receive(this,event); } } } // class DefaultGenerator
Lets see why this is correct. First, the synchronized(lock) blocks synchronize access to the listeners list. It is very important that only the clone step of send is synchronized. This is because completing a send step may require arbitrary adds or removes. If these adds or removes happen from a different thread compared to the send, they would deadlock if send(Event) is synchronized. To avoid this problem, we copy the listeners list before notifying them, thus eliminating the resource contention. It is also important that we do not use the synchronized keyword in front of the add, remove, and clone methods. This is because using synchronized in front of a method uses the object itself for mutually exclusive access to the methods. Since you have no control over how the DefaultGenerator will be used, it is pretty easy to set up a deadlock situation by having this public mutex locked by something else. By sticking to an internal private object for a lock, we know we’re the only ones who affect it.
So safety pays in correctness (which is a good thing), but it suffers from some serious inefficiencies:
Sometimes correctness costs efficiency, but we are lucky here, because we can have both:
class DefaultGenerator implements Generator {
private static final Listener[] NONE = new Listener[0];
private Listener [] listeners = NONE;
private Object lock = new Object();
private boolean contains(Listener listener) {
Listener[] tmp = listeners;
for (int i=tmp.length-1; i>=0; --i)
if (tmp[i] == listener) return true;
return false;
}
// if we don't already have it,
// add a listener to the front of a new listeners array.
public void addListener(Listener listener) {
synchronized(lock) {
if (listener == null || contains(listener)) return;
Listener[] tmp = new Listener[listeners.length+1];
for (int i=tmp.length-1; i>0; --i) tmp[i]=listeners[i-1];
tmp[0]=listener;
listeners=tmp; // atomic update
}
}
// if we do already have it,
// remove a listener from a new listeners array.
public void removeListener(Listener listener) {
synchronized(lock) {
if (!contains(listener)) return;
Listener[] tmp = (listeners.length > 1) ?
new Listener[listeners.length-1] : NONE;
for (int i=listeners.length-1,j=i; i >= 0; --i)
if (listeners[i] != listener)
tmp[--j]=listeners[i];
listeners=tmp; // atomic update
}
}
protected final boolean listening() {
return listeners != NONE;
}
protected final void send(Event event) {
Listener[] tmp=listeners; // atomic read
for (int i=tmp.length-1; i>=0; --i) {
tmp[i].receive(this,event);
}
}
} // class DefaultGenerator
To use this DefaultGenerator, we would write:
class MyGenerator extends DefaultGenerator {
void send() { if (listening()) send(new Event()); }
}
There is more code, but what have we gained? This implementation is also correct, but look at send(), listening() and send(Event): no synchronization, no copies, no type-casts, and we only create an Event if (almost always - see callout) we have listeners to receive it. Instead of duplicating the listener list every time a send happens, we instead make copies when adding and removing from the listeners array. This works because object reference assignment is an atomic action. We use a plain old array because this eliminates unnecessary run-time type casts and the iterator overhead.
I stated that, almost always,
if (listening()) send(new Event())will create and send an event only when there are listeners to receive it. But notice that there is no synchronization between the listening() and the send(new Event()), and so the listeners list may change arbitrarily between the check for listeners and the send. First, realize this is a very subtle timing issue and so, almost always, the listening() call will correctly determine if there is anyone listening. Second, realize this does not affect the correctness of the generators, because only two things will (very rarely) happen:
listening() returns true, but the last listener is removed before the send(Event).
The Event is created an unused. This has no impact on correctness, and negligible effect on efficiency.
listening() returns false, which skips a send(Event), because a listener is added just before the send would have occurred.
This same argument could be made in the send(Event) method itself, since we copy the reference to the listeners array before notifying listeners. At some point (shortly before the sending) you have to decide if you will or won't actually send. Since it is convenient to decide if you will send before constructing the event, this has no impact on correctness, but a significant (positive) impact on efficiency.
With the generics at hand, you can stop worrying about making correct and fast generators and just use them. For example, if you wanted to a DateGenerator that sends the current Date on every tick(), a listener that printed out the date, and some main code to put them together:
class DateGenerator extends DefaultGenerator < Date > { public void tick() { if (listening()) send(new Date()); } } class DateListener implements Listener < Date > { public void receive(Generator < Date > generator , Date date) { System.out.println(date); } } public class Main { public static void main(String[] args) { DateGenerator generator = new DateGenerator(); DateListener listener = new DateListener(); generator.addListener(listener); generator.tick(); } }
With the generic implementation, you can think about the actual messages and what they do, rather than the details of the mechanism for every new message type.
For those who are required to be compatible with a pre 1.5 JDK, it can be a lot of error prone typing to write the boiler-plate code for every Event type you happen to need. In the resources, there is a tool for writing this code (built using the BeanShell Preprocessor, but you don’t need BPP or BeanShell to use it).
So. Generators are poorly or incorrectly implemented all the time. Using these generators result in solutions that are less stable, slower, and consume more memory. For older (pre 1.5) JDK’s, this can be solved by adopting the fast generator pattern. These patterns can be built automatically using meta.GenerateListeners. For new (post 1.5) JDK’s, this can be solved using generic classes. These tools are given in the resources.
That’s it: happy listening in a dynamic world!