Events in Java

Monday, June 1. 2009

Events in Java

Java has no integrated event system but most classes in the Java Runtime uses the Listener Pattern for this. This pattern is pretty powerful and (when using Generics) completely type-safe but in the end it is still pain-in-the-ass. Think of a class which wants to send five different events. You have to write five listener interfaces and write at least 15 methods (five register methods, five unregister methods and five fire-event methods) and collections for all the listener objects which are registered. Depending on the type of events you may want to write five event classes which are used as containers for the event data. Lazy programmers therefor put all five events into the same listener interface so they only need one register/unregister/fire tripplet. But this requires the event receivers to implement all five listener methods even if they are interested in only one of them. So some genius invented the Adapter classes which already provide empty listener methods which can be extended by the event receiver. The whole thing is pretty annoying because it's so complicated to implement and to use.

So lets take a look at a different approach...

In the following examples we will write a small program which works with two data classes: A Citizen class and a BigBrother class. A citizen has a name and can be ordered to drink something. Everytime the citizen drinks something it sends an event which contains a reference to the citizen and the drink. The BigBrother class only contains an event receiver method which checks the drink and decides if the citizen is allowed to drink this stuff or if the police must be called. Yay, what a useless example...

The main method

Let's begin with the main method:

public static void main(final String[] args)
{
    final Citizen winston = new Citizen("Winston");
    final Citizen julia = new Citizen("Julia");
    
    final BigBrother bigBrother = new BigBrother();
    
    winston.connect(bigBrother);
    julia.connect(bigBrother);
    
    winston.drink("milk");
    julia.drink("tea");
}

As you can see there is only one line of code here to connect the big brother to the citizen. But the Listener Pattern also had only one line of code here (Something like addDrinkListener(bigBrother) for example). You may wonder why the called method has the generic name connect. What about the event type the bigBrother wants to listen for? This will become clear if you take a look at the BigBrother class.

The BigBrother class
public class BigBrother
{
    @DrinkSignal.Slot
    public void checkDrink(final DrinkSignal signal)
    {
        final String name = signal.citizen.getName();
        final String what = signal.what;

        if ("milk".equals(what))
        {
            System.out.println("Big Brother thinks it's ok if " + name
                + " drinks " + what);
        }
        else
        {
            System.out.println("Big Brother calls the police because " + name
                + " drinks " + what);
        }
    }
}

The event type (a Signal) is defined by using annotations. Slot methods always have the same signature: They must be public, they must await a single argument which is the Signal class and it must be annotated with the inner Slot class of the same Signal class of the one and only parameter. The name of the method doesn't matter. And it does not matter how many listener methods are in the class. Multiple slot methods can listen for the same signals.

The DrinkSignal class

Now let's take a look at the DrinkSignal class:

public class DrinkSignal
{
    @Retention(RetentionPolicy.RUNTIME) public @interface Slot {}

    public final Citizen citizen;
    public final String what;

    public DrinkSignal(final Citizen citizen, final String what)
    {
        this.citizen = citizen;
        this.what = what;
    }
}

The only requirement for a Signal class is the inner annotation class named Slot with retention policy RUNTIME. Everything else is up to you. It's just a plain class.

The Citizen class

And now comes the interesting part: The Citizen class. Let's see how this class sends the signal:

public class Citizen
{
    private final Signals signals = new Signals();

    private final String name;

    public Citizen(final String name)
    {
        this.name = name;
    }

    public void connect(final Object slotObject)
    {
        this.signals.connect(slotObject);
    }

    public void drink(final String what)
    {
        System.out.println("Citizen " + this.name + " drinks " + what);
        this.signals.send(new DrinkSignal(this, what));
    }

    public String getName()
    {
        return this.name;
    }
}

So here we have some more lines which are responsible for the event system. First of all this class has a property named signals which is an instance of the Signals class which we will see later. And there is a simple connect() method which simply delegates to the signal instance. You can do the same with the disconnect() method if you need it. And last but not least there is a call to the signals.send() method. The one and only parameter to this method is the signal itself which will be received by all connected slot methods.

The clue is that no more bloat-code is needed in the class to have more then this single signal. If you want the citizen to signal what he eats and when he sleeps you just write a single Signal class for each signal you want to send and then add a corresponding signals.send() line whereever you want to send this signal. That's all. No need for thousands of addListener/removeListener/fireEvent methods. The one and only connect() method is enough.

The Signals class

One class left. The Signals class ist the workhorse of the whole system. It manages the list of connected slot objects and uses the reflection API to find and call matching slot methods. Here we go:

public class Signals
{
    private final List objects = new ArrayList();

    public void connect(final Object object)
    {
        this.objects.add(object);
    }

    public void disconnect(final Object object)
    {
        this.objects.remove(object);
    }

    @SuppressWarnings("unchecked")
    public void send(final Object signal)
    {
        final String signalClassName = signal.getClass().getName();
        try
        {
            final Class slotAnno = (Class) Class
                .forName(signalClassName + "$Slot");

            for (final Object object : this.objects)
            {
                for (final Method method : object.getClass().getMethods())
                {
                    if (method.isAnnotationPresent(slotAnno))
                    {
                        method.invoke(object, signal);
                    }
                }
            }
        }
        catch (final ClassNotFoundException e)
        {
            throw new RuntimeException("Signal " + signalClassName
                + " has no inner Slot class: " + e, e);
        }
        catch (final IllegalArgumentException e)
        {
            throw new RuntimeException("Unable to run slot method: " + e, e);
        }
        catch (final IllegalAccessException e)
        {
            throw new RuntimeException("Unable to run slot method: " + e, e);
        }
        catch (final InvocationTargetException e)
        {
            throw new RuntimeException("Unable to run slot method: " + e, e);
        }
    }
}

So there is one array list which holds the connected slot objects, a connect() and a disconnect() method for adding and removing entries in this list and there is the send() method (which looks bloated because of the exception handling...) which simply iterates over all methods of the connected objects and runs the methods which are annotated as slots for the signal which is to be send.

That's all. I did not run any performance tests yet so maybe the iteration over all methods of all connected objects may be too slow. A getAnnotatedMethods() method in the reflection API would be cool... But at least it is much easier to use than this darn Listener Pattern.

The output

Oh, one more thing: Executing this pretty useless program produces the following output:

Citizen Winston drinks milk
Big Brother thinks it's ok if Winston drinks milk
Citizen Julia drinks tea
Big Brother calls the police because Julia drinks tea
Posted in Java | Comments (0)

Enclosing asterisks marks text as bold (*word*), underscore are made via _word_.