1. When you need custom events
Standard Java events like button clicks, mouse movement, or text changes are just the tip of the iceberg. In real applications, there are many situations that don’t fit into these frames. For example, a program may load data from the internet and you need to notify other parts of the application when the load is complete. In a game, a player can earn a new achievement, and this should be reported to several components at once, such as the UI and the logging system. In a business application, a change in order state should trigger notifications for accounting, the warehouse, and the user simultaneously.
In all such cases, it’s useful to create your own event types and listeners. They allow you to describe unique interaction scenarios between components and make the code more flexible and structured.
Structure of a custom event
To create your own event in Java, you typically implement three parts:
- Event class — typically a subclass of java.util.EventObject. Stores information about the event (who the source is, what data is associated with the event).
- Listener interface — for example, MyEventListener, which extends java.util.EventListener and defines methods to handle the event.
- Subscription/unsubscription mechanism — methods on the event source for add...Listener/remove...Listener and invoking listeners when the event occurs (fire...).
Let’s walk through this step by step using an example of an application that loads data from a file or the network and needs to notify others when the loading is complete.
Event class
Let’s create an event class that will contain information about the completed load.
import java.util.EventObject;
// Event class: extend EventObject
public class DataLoadedEvent extends EventObject {
private final String data; // Additional information about the event
public DataLoadedEvent(Object source, String data) {
super(source); // source - the object that triggered the event
this.data = data;
}
public String getData() {
return data;
}
}
Here, source is the event source object (for example, a data loader), and data is a string with the loaded data (this can be a file path, JSON, a result, etc.).
Listener interface
Define the listener interface. Usually it extends EventListener (a marker interface for typing).
import java.util.EventListener;
// Listener interface for our event
public interface DataLoadedListener extends EventListener {
void dataLoaded(DataLoadedEvent event);
}
The dataLoaded method will be called when the event occurs.
Event source
We need an entity that will store the list of listeners, allow registering/removing them, and notify them when the event occurs.
import java.util.ArrayList;
import java.util.List;
public class DataLoader {
private final List<DataLoadedListener> listeners = new ArrayList<>();
// Register a listener
public void addDataLoadedListener(DataLoadedListener listener) {
listeners.add(listener);
}
// Remove a listener
public void removeDataLoadedListener(DataLoadedListener listener) {
listeners.remove(listener);
}
// Method that fires the event (e.g., after data is loaded)
private void fireDataLoaded(String data) {
DataLoadedEvent event = new DataLoadedEvent(this, data);
// Notify all listeners
for (DataLoadedListener listener : listeners) {
listener.dataLoaded(event);
}
}
// Example method that "loads" data
public void loadData() {
// Simulate loading (e.g., from a file or the network)
String loadedData = "This is the loaded data!";
System.out.println("Data loaded: " + loadedData);
// Notify all listeners
fireDataLoaded(loadedData);
}
}
In real life, the loadData() method may be asynchronous, read a file, call a server, etc., but for this example we just simulate loading.
2. Using a custom event
Suppose we have a component that wants to be notified when data loading completes.
public class DataLoadedHandler implements DataLoadedListener {
@Override
public void dataLoaded(DataLoadedEvent event) {
System.out.println("Handler received event: " + event.getData());
}
}
Wire everything together in the application's main class:
public class Main {
public static void main(String[] args) {
DataLoader loader = new DataLoader();
DataLoadedHandler handler = new DataLoadedHandler();
// Register the listener
loader.addDataLoadedListener(handler);
// Start loading data
loader.loadData();
}
}
What will happen?
- DataLoader loads the data (simulation).
- After loading, it calls fireDataLoaded, which creates an event object and notifies all listeners.
- Our handler (DataLoadedHandler) receives the event and prints a message.
Sample output:
Data loaded: This is the loaded data!
Handler received event: This is the loaded data!
Using anonymous classes and lambda expressions
To avoid creating separate classes for each listener, it’s common to use anonymous classes or lambda expressions (starting with Java 8):
public class Main {
public static void main(String[] args) {
DataLoader loader = new DataLoader();
// Listener via a lambda expression
loader.addDataLoadedListener(event ->
System.out.println("Lambda handler: " + event.getData())
);
loader.loadData();
}
}
Registering and removing listeners
Listeners can be added and removed. This is important for memory management and preventing leaks (especially if the listener is a “heavy” object or is no longer needed).
DataLoadedHandler handler = new DataLoadedHandler();
loader.addDataLoadedListener(handler);
// Later, if the handler is no longer needed:
loader.removeDataLoadedListener(handler);
If you don’t remove a listener while the event source lives for a long time, the listener will remain in the list and won’t be garbage-collected — this may lead to a memory leak.
3. Practice: mini example — click counter
Let’s build a small example that you can expand in a learning application. Suppose we have a counter class that increments its value and, on each increment, notifies listeners about the new value.
Event class
import java.util.EventObject;
public class CounterChangedEvent extends EventObject {
private final int newValue;
public CounterChangedEvent(Object source, int newValue) {
super(source);
this.newValue = newValue;
}
public int getNewValue() {
return newValue;
}
}
Listener interface
import java.util.EventListener;
public interface CounterChangedListener extends EventListener {
void counterChanged(CounterChangedEvent event);
}
Counter class
import java.util.ArrayList;
import java.util.List;
public class Counter {
private int value = 0;
private final List<CounterChangedListener> listeners = new ArrayList<>();
public void addCounterChangedListener(CounterChangedListener listener) {
listeners.add(listener);
}
public void removeCounterChangedListener(CounterChangedListener listener) {
listeners.remove(listener);
}
public void increment() {
value++;
fireCounterChanged();
}
private void fireCounterChanged() {
CounterChangedEvent event = new CounterChangedEvent(this, value);
for (CounterChangedListener listener : listeners) {
listener.counterChanged(event);
}
}
}
Usage
public class Main {
public static void main(String[] args) {
Counter counter = new Counter();
// Register a listener via a lambda
counter.addCounterChangedListener(event ->
System.out.println("Counter changed: " + event.getNewValue())
);
counter.increment(); // Counter changed: 1
counter.increment(); // Counter changed: 2
}
}
4. Common mistakes when creating and handling custom events
Error #1: forgot to call the listener notification method. An event is created, but the method that should notify listeners (fireDataLoaded, fireCounterChanged) isn’t called. As a result, listeners stay silent.
Error #2: exceptions in listener handlers. If one of the listeners throws an exception, the rest may not receive notifications. A good practice is to wrap listener calls in try–catch so that one “bad” listener doesn’t interfere with the others.
Error #3: not removing listeners. If a listener is no longer needed but wasn’t removed, it continues to receive events and isn’t removed from memory. This can lead to memory leaks — especially when the source lives a long time and there are many listeners.
Error #4: modifying the listener list while iterating. If, inside an event handler, someone adds or removes a listener, it may lead to a ConcurrentModificationException. A safe approach is to copy the list into a separate array first, then iterate over the copy.
Error #5: long-running operations in handlers. If a handler does something long-running (file download, waiting for the network), the UI may “freeze.” Move heavy tasks to a separate thread or use asynchronous mechanisms.
GO TO FULL VERSION