1. Wenn eigene Ereignisse nötig sind
Standardereignisse von Java wie das Klicken auf eine Schaltfläche, Mausbewegungen oder Textänderungen sind nur die Spitze des Eisbergs. In realen Anwendungen gibt es viele Situationen, die nicht in diese Schablonen passen. Zum Beispiel lädt ein Programm Daten aus dem Internet und andere Teile der Anwendung müssen informiert werden, sobald das Laden abgeschlossen ist. In einem Spiel kann ein Spieler eine neue Errungenschaft (Achievement) erhalten, und darüber sollten mehrere Komponenten informiert werden, etwa die Benutzeroberfläche und das Logging-System. In einer Business-Anwendung sollte eine Zustandsänderung einer Bestellung Benachrichtigungen für Buchhaltung, Lager und Benutzer gleichzeitig auslösen.
In all diesen Fällen ist es sinnvoll, eigene Ereignistypen und Listener zu erstellen. Sie ermöglichen es, einzigartige Interaktionsszenarien zwischen Komponenten zu beschreiben und den Code flexibler und strukturierter zu gestalten.
Struktur eines eigenen Ereignisses
Um ein eigenes Ereignis in Java zu erstellen, implementiert man gewöhnlich drei Teile:
- Ereignisklasse — in der Regel ein Subtyp von java.util.EventObject. Sie speichert Informationen über das Ereignis (wer die Quelle ist, welche Daten mit dem Ereignis verbunden sind).
- Listener-Interface — zum Beispiel MyEventListener, das java.util.EventListener erweitert und Methoden zur Verarbeitung des Ereignisses definiert.
- Mechanismus zum Abonnieren/Abmelden — Methoden in der Ereignisquelle für add...Listener/remove...Listener sowie das Aufrufen der Listener beim Eintreten des Ereignisses (fire...).
Schauen wir uns das Schritt für Schritt am Beispiel einer Anwendung an, in der Daten aus einer Datei oder dem Netzwerk geladen werden und wir andere über den Abschluss des Ladevorgangs benachrichtigen müssen.
Ereignisklasse
Wir erstellen eine Ereignisklasse, die Informationen über einen abgeschlossenen Ladevorgang enthält.
import java.util.EventObject;
// Ereignisklasse: erbt von EventObject
public class DataLoadedEvent extends EventObject {
private final String data; // Zusätzliche Informationen zum Ereignis
public DataLoadedEvent(Object source, String data) {
super(source); // source - das Objekt, das das Ereignis ausgelöst hat
this.data = data;
}
public String getData() {
return data;
}
}
Hier ist source das Objekt, das die Ereignisquelle darstellt (z. B. der Datenlader), und data ist eine Zeichenkette mit den geladenen Daten (dies kann ein Dateipfad, JSON, ein Ergebnis usw. sein).
Listener-Interface
Definieren wir das Listener-Interface. Üblicherweise erweitert es EventListener (ein Marker-Interface zur Typisierung).
import java.util.EventListener;
// Listener-Interface für unser Ereignis
public interface DataLoadedListener extends EventListener {
void dataLoaded(DataLoadedEvent event);
}
Die Methode dataLoaded wird aufgerufen, wenn das Ereignis eintritt.
Ereignisquelle
Wir benötigen eine Entität, die die Liste der Listener hält, deren Registrierung/Entfernung ermöglicht und beim Eintreten des Ereignisses benachrichtigt.
import java.util.ArrayList;
import java.util.List;
public class DataLoader {
private final List<DataLoadedListener> listeners = new ArrayList<>();
// Listener registrieren
public void addDataLoadedListener(DataLoadedListener listener) {
listeners.add(listener);
}
// Listener entfernen
public void removeDataLoadedListener(DataLoadedListener listener) {
listeners.remove(listener);
}
// Methode, die das Ereignis auslöst (z. B. nach dem Laden der Daten)
private void fireDataLoaded(String data) {
DataLoadedEvent event = new DataLoadedEvent(this, data);
// Alle Listener benachrichtigen
for (DataLoadedListener listener : listeners) {
listener.dataLoaded(event);
}
}
// Beispielmethode, die Daten "lädt"
public void loadData() {
// Wir simulieren das Laden (z. B. aus Datei oder Netzwerk)
String loadedData = "Dies sind die geladenen Daten!";
System.out.println("Daten geladen: " + loadedData);
// Alle Listener benachrichtigen
fireDataLoaded(loadedData);
}
}
In der Praxis kann die Methode loadData() asynchron sein, eine Datei lesen, einen Server ansprechen usw.; für das Beispiel simulieren wir den Ladevorgang lediglich.
2. Verwendung des eigenen Ereignisses
Stellen wir uns vor, wir haben eine Komponente, die über den Abschluss des Datenladens informiert werden möchte.
public class DataLoadedHandler implements DataLoadedListener {
@Override
public void dataLoaded(DataLoadedEvent event) {
System.out.println("Handler hat ein Ereignis erhalten: " + event.getData());
}
}
Bringen wir alles in der Hauptklasse der Anwendung zusammen:
public class Main {
public static void main(String[] args) {
DataLoader loader = new DataLoader();
DataLoadedHandler handler = new DataLoadedHandler();
// Listener registrieren
loader.addDataLoadedListener(handler);
// Daten laden starten
loader.loadData();
}
}
Was passiert?
- DataLoader lädt Daten (Simulation).
- Nach dem Laden wird fireDataLoaded aufgerufen, das ein Ereignisobjekt erstellt und alle Listener benachrichtigt.
- Unser Handler (DataLoadedHandler) erhält das Ereignis und gibt eine Meldung aus.
Beispielausgabe:
Daten geladen: Dies sind die geladenen Daten!
Handler hat ein Ereignis erhalten: Dies sind die geladenen Daten!
Verwendung anonymer Klassen und Lambda-Ausdrücke
Um nicht für jeden Listener separate Klassen zu erstellen, nutzt man häufig anonyme Klassen oder Lambda-Ausdrücke (seit Java 8):
public class Main {
public static void main(String[] args) {
DataLoader loader = new DataLoader();
// Listener über einen Lambda-Ausdruck
loader.addDataLoadedListener(event ->
System.out.println("Lambda-Handler: " + event.getData())
);
loader.loadData();
}
}
Registrieren und Entfernen von Listenern
Listener können hinzugefügt und entfernt werden. Das ist wichtig für das Speichermanagement und zur Vermeidung von Lecks (insbesondere wenn der Listener ein „schweres“ Objekt ist oder nicht mehr benötigt wird).
DataLoadedHandler handler = new DataLoadedHandler();
loader.addDataLoadedListener(handler);
// Später, wenn der Handler nicht mehr benötigt wird:
loader.removeDataLoadedListener(handler);
Wenn man einen Listener nicht entfernt, während die Ereignisquelle lange lebt, bleibt der Listener in der Liste und wird nicht vom Garbage Collector entfernt – das kann zu Memory-Leaks führen.
3. Praxis: Mini-Beispiel – Klickzähler
Wir bauen ein kleines Beispiel, das sich in einem Lernprojekt weiterentwickeln lässt. Nehmen wir an, wir haben eine Zählerklasse, die ihren Wert erhöht und bei jeder Erhöhung die Listener über den neuen Wert informiert.
Ereignisklasse
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);
}
Zählerklasse
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);
}
}
}
Verwendung
public class Main {
public static void main(String[] args) {
Counter counter = new Counter();
// Listener per Lambda registrieren
counter.addCounterChangedListener(event ->
System.out.println("Zähler hat sich geändert: " + event.getNewValue())
);
counter.increment(); // Zähler hat sich geändert: 1
counter.increment(); // Zähler hat sich geändert: 2
}
}
4. Typische Fehler bei Erstellung und Verarbeitung eigener Ereignisse
Fehler Nr. 1: Die Benachrichtigungsmethode für Listener wurde vergessen. Das Ereignis wird erzeugt, aber die Methode, die die Listener benachrichtigen soll (fireDataLoaded, fireCounterChanged), wird nicht aufgerufen. Infolgedessen „schweigen“ die Listener.
Fehler Nr. 2: Ausnahmen in Listener-Handlern. Wenn einer der Listener eine Ausnahme wirft, erhalten die übrigen ggf. keine Benachrichtigung. Gute Praxis ist es, die Aufrufe der Listener in try–catch zu kapseln, damit ein „schlechter“ Listener die anderen nicht beeinträchtigt.
Fehler Nr. 3: Listener werden nicht entfernt. Wenn ein Listener nicht mehr benötigt wird, aber nicht entfernt wurde, erhält er weiterhin Ereignisse und bleibt im Speicher. Das kann zu Memory-Leaks führen – insbesondere wenn die Quelle lange lebt und es viele Listener gibt.
Fehler Nr. 4: Änderung der Listenerliste während der Iteration. Wenn in einem Handler jemand einen Listener hinzufügt oder entfernt, kann dies zu ConcurrentModificationException führen. Ein sicherer Ansatz ist es, die Liste zunächst in ein separates Array zu kopieren und dann über die Kopie zu iterieren.
Fehler Nr. 5: Langlaufende Operationen in Handlern. Wenn ein Handler etwas Zeitaufwändiges tut (Datei laden, auf das Netzwerk warten), kann die Oberfläche „einfrieren“. Verlagern Sie schwere Aufgaben in einen separaten Thread oder verwenden Sie asynchrone Mechanismen.
GO TO FULL VERSION