CodeGym /Kurse /JAVA 25 SELF /Erstellen und Verarbeiten eigener Ereignisse

Erstellen und Verarbeiten eigener Ereignisse

JAVA 25 SELF
Level 50 , Lektion 2
Verfügbar

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:

  1. 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).
  2. Listener-Interface — zum Beispiel MyEventListener, das java.util.EventListener erweitert und Methoden zur Verarbeitung des Ereignisses definiert.
  3. 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?

  1. DataLoader lädt Daten (Simulation).
  2. Nach dem Laden wird fireDataLoaded aufgerufen, das ein Ereignisobjekt erstellt und alle Listener benachrichtigt.
  3. 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 trycatch 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.

Kommentare
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION