An improved version and source code on my personal website: How to code events in Java
A few years ago, back when ActionScript 3 was still used, but barely, I wanted to try game development in Java. All major game engines use other programming languages like C#, C++ or Lua, so it was a real pain to get started. That is when I discovered LibGDX, a graphics library for Java. The downside is that LibGDX is not very friendly for beginners and for someone that used to do game dev in ActionScript 3, it was a bit confusing.
That is why I wanted to see if I can simplify things a bit and to try and reproduce the easy (for me at least) API that ActionScript 3 had for handling graphical elements. My main problem was not the actual way of creating elements on the scene, but the fact that it was really complicated to nest one object inside another in order to create a more complex one. I wanted to see if I can reproduce the easiness of doing this from AS3 to Java so I got started on two projects. Sadly I did not have time to finish any of them, but I did learn quite a few things on the way.
One of the first functionalities that I designed in my head was event handling. In any game, the possibility to trigger events and react to them is extremely important. Any key press is an event, any frame render is an event, and there are many more. Here I will show you how to create your own event handling in Java. Keep in mind that this is just a small, easy-to-understand version. Event handling can be quite complex, but it should be enough for medium projects, for learning, and for training your mind.
The Observer Pattern
If you are familiar with design patterns, you most probably heard of the Observer Pattern. This is the basic principle that stands at the foundation of any event-based framework. It consists of a subject that maintains a list of observers and anytime a certain action/event is happening, it notifies the observers. You can read more about this pattern here.
A more robust and complex system derived from the Observer Pattern is what we will use. Even though the code is more complex and we have more classes and more control over what happens, the general idea is the same.
So, let’s get started.
Event types in Java
First thing, we need to have some event types. These can be identified as simply Strings, but to have cleaner code (and to make sure we don’t submit invalid types), it is best to have an interface ‘EventType’. No methods are needed in the interface. We just need it as a type-constraint for when registering a listener for an event. The actual event types will be Enums that implement our specific interface. Keep in mind that Enums can’t be extended, but they can implement an interface. For our example, I will create only a LoggerEvent with three values: INFO, WARNING and ERROR. These are events for which I will be able to register.
public interface EventType {
}
public enum LoggerEvent implements EventType {
ERROR,
WARNING,
INFO;
}
Creating the event
It is important that when an event is triggered we have the information that we need. Usually, we will need the source of the event, the event type and additional data that is specific for the event itself. For now, let’s consider the following event class.
public class Event {
private EventType type;
private EventDispatcher source;
private Object[] eventData;
public Event(EventType type, EventDispatcher source, Object... eventData) {
this.type = type;
this.source = source;
this.eventData = eventData;
}
}
Dispatching the event
The biggest hurdle we need to overcome is dispatching the event to any listener. I will be having an EventDispatcher class that does all this. It will hold the listeners that are registered to it, it can dispatch the event to all listeners and it will build the Event object. There are a few things that you need to take into consideration.
First, make sure you register each listener once only. Due to bad programming, unknowing the code or a simple mistake, there can be scenarios where the same listener is registered twice. Always check that the listener was not already added prior to registering it.
Next, error handling. When an event is dispatched, the listener can thrown an exception. If not properly handled this will break the iteration over registered listeners and not all will receive the event. This can happen either because of external reasons, bugs in the code, or improper handling of the event data. Below is what I came up with.
public class EventDispatcher {
private Map<EventType, List<EventListener>> listeners;
public EventDispatcher() {
this.listeners = new HashMap<>();
}
public void addEventListener(EventType eventType, EventListener listener) {
if (listener == null) return;
List<EventListener> handlersForEventType = listeners.get(eventType);
if (handlersForEventType == null) {
handlersForEventType = new ArrayList<>();
listeners.put(eventType, handlersForEventType);
}
if (handlersForEventType.contains(listener)) return;
handlersForEventType.add(listener);
}
protected void dispatchEvent(EventType eventType, Object... eventData) {
List<EventListener> listenersForEvent = listeners.get(eventType);
if (listenersForEvent == null || listenersForEvent.isEmpty()) return;
listenersForEvent.forEach(listener -> {
try {
listener.onEvent(buildEvent(eventType, eventData));
} catch (Exception e) {
System.err.println("Listener threw exception for event " + eventType);
}
});
}
protected Event buildEvent(EventType eventType, Object... eventData) {
return new Event(eventType, this, eventData);
}
}
You may have noticed that the map contains lists of EventListeners. These are the objects that will receive the Event. These are the objects that will subscribe to the handler for receiving events. EventListener is just an interface that has an onEvent() method.
public interface EventListener {
void onEvent(Event event);
}
As a note, keep in mind that in the current implementation a listener won’t receive the event until the previous listeners finish their processing. This is not the right way of doing things. Instead, the dispatcher should send-and-forget, moving on the next listener as soon as the event was sent. For simplicity, I won’t be handling this scenario here.
Putting it all together
Now that we have all the classes we need to be able to trigger and handle events, let’s create our logger. For simplicity, the logger will only have three methods that will print the message received. The methods will correspond to errors, warnings and information messages. Since the logger will be able to send events, it will extend our EventDispatcher class.
public class MyCustomLogger extends EventDispatcher {
public void error(String message) {
System.err.println("ERROR: " + message);
dispatchEvent(LoggerEventType.ERROR, message);
}
public void warning(String message) {
System.out.println("WARN: " + message);
dispatchEvent(LoggerEventType.WARNING, message);
}
public void info(String message) {
System.out.println("INFO: " + message);
dispatchEvent(LoggerEventType.INFO, message);
}
}
The beauty in this approach is that we have all the needed functionality to register a listener and to dispatch the event already written. Now, we can just create our Listener. In this example, I assumed that we want an email to be sent whenever an error is logged and that the email should contain the error message. So, we have this simple listener.
public class ErrorEmailSender implements EventListener {
@Override
public void onEvent(Event event) {
sendEmail((String) event.getEventData()[0]);
}
private void sendEmail(String errorMessage) {
System.out.println("Sending email with: " + errorMessage);
}
}
Now, I know what you are thinking: “Do I need to have a new class so that I can handle the event?” You do not. You can use lambdas to achieve this without the need for a new class. Here is an example of the two approaches.
public static void main(String[] argv) {
MyCustomLogger logger = new MyCustomLogger();
logger.addEventListener(LoggerEventType.ERROR, new ErrorEmailSender());
logger.addEventListener(LoggerEventType.INFO, l -> {System.out.println("From Warning Listener: " + l.getEventData()[0]);});
logger.info("Only from logger will show");
logger.warning("A warning message");
logger.error("An error message");
}
For further work and improvements on the above version, read the full article on my personal blog: How to code events in Java
Top comments (0)