CodeGym /Kursy /JAVA 25 SELF /Tworzenie i obsługa własnych zdarzeń

Tworzenie i obsługa własnych zdarzeń

JAVA 25 SELF
Poziom 50 , Lekcja 2
Dostępny

1. Kiedy potrzebne są własne zdarzenia

Standardowe zdarzenia Javy, takie jak kliknięcie przycisku, ruch myszy czy zmiana tekstu, to tylko wierzchołek góry lodowej. W rzeczywistych aplikacjach pojawia się mnóstwo sytuacji, które nie mieszczą się w tych ramach. Na przykład program może pobierać dane z internetu i trzeba powiadomić inne części aplikacji, gdy ładowanie zostanie zakończone. W grze gracz może zdobyć nowe osiągnięcie i warto o tym poinformować kilka komponentów naraz, na przykład interfejs i system logowania. W aplikacji biznesowej zmiana stanu zamówienia powinna wyzwalać powiadomienia dla działu księgowości, magazynu i użytkownika jednocześnie.

We wszystkich takich przypadkach warto tworzyć własne typy zdarzeń i słuchaczy. Pozwalają one opisać unikalne scenariusze interakcji komponentów oraz uczynić kod bardziej elastycznym i uporządkowanym.

Struktura zdarzenia własnego

Aby utworzyć własne zdarzenie w Javie, zwykle implementuje się trzy elementy:

  1. Klasa zdarzenia — zazwyczaj potomna java.util.EventObject. Przechowuje informacje o zdarzeniu (kto jest źródłem, jakie dane są z nim związane).
  2. Interfejs słuchacza — na przykład MyEventListener, który rozszerza java.util.EventListener i definiuje metody obsługi zdarzenia.
  3. Mechanizm subskrypcji/wyrejestrowania — metody w źródle zdarzenia do add...Listener/remove...Listener oraz wywołanie słuchaczy w momencie zajścia zdarzenia (fire...).

Przyjrzyjmy się temu krok po kroku na przykładzie aplikacji, w której dane są wczytywane z pliku lub sieci, a my musimy powiadamiać innych o zakończeniu ładowania.

Klasa zdarzenia

Utwórzmy klasę zdarzenia, która będzie zawierać informacje o zakończonym wczytywaniu.

import java.util.EventObject;

// Klasa zdarzenia: dziedziczymy po EventObject
public class DataLoadedEvent extends EventObject {
    private final String data; // Dodatkowe informacje o zdarzeniu

    public DataLoadedEvent(Object source, String data) {
        super(source); // source — obiekt, który wywołał zdarzenie
        this.data = data;
    }

    public String getData() {
        return data;
    }
}

Tutaj source to obiekt-źródło zdarzenia (np. ładowarka danych), a data to ciąg z wczytanymi danymi (może to być ścieżka do pliku, JSON, wynik itd.).

Interfejs słuchacza

Zdefiniujmy interfejs słuchacza. Zwykle rozszerza on EventListener (interfejs znacznikowy do typowania).

import java.util.EventListener;

// Interfejs słuchacza naszego zdarzenia
public interface DataLoadedListener extends EventListener {
    void dataLoaded(DataLoadedEvent event);
}

Metoda dataLoaded będzie wywoływana, gdy zdarzenie nastąpi.

Źródło zdarzenia

Potrzebna jest jednostka, która będzie przechowywać listę słuchaczy, pozwalać na ich rejestrację/usuwanie oraz powiadamiać w momencie zajścia zdarzenia.

import java.util.ArrayList;
import java.util.List;

public class DataLoader {
    private final List<DataLoadedListener> listeners = new ArrayList<>();

    // Rejestracja słuchacza
    public void addDataLoadedListener(DataLoadedListener listener) {
        listeners.add(listener);
    }

    // Usunięcie słuchacza
    public void removeDataLoadedListener(DataLoadedListener listener) {
        listeners.remove(listener);
    }

    // Metoda, która inicjuje zdarzenie (np. po załadowaniu danych)
    private void fireDataLoaded(String data) {
      DataLoadedEvent event = new DataLoadedEvent(this, data);
        // Powiadamiamy wszystkich słuchaczy
        for (DataLoadedListener listener : listeners) {
            listener.dataLoaded(event);
        }
    }

    // Przykładowa metoda, która "ładuje" dane
    public void loadData() {
        // Symulujemy ładowanie (np. z pliku lub sieci)
        String loadedData = "To są wczytane dane!";
        System.out.println("Dane wczytane: " + loadedData);

        // Informujemy wszystkich słuchaczy
        fireDataLoaded(loadedData);
    }
}

W rzeczywistości metoda loadData() może być asynchroniczna, czytać plik, łączyć się z serwerem itd., ale na potrzeby przykładu po prostu symulujemy ładowanie.

2. Używanie własnego zdarzenia

Załóżmy, że mamy komponent, który chce wiedzieć o zakończeniu wczytywania danych.

public class DataLoadedHandler implements DataLoadedListener {
    @Override
    public void dataLoaded(DataLoadedEvent event) {
        System.out.println("Handler otrzymał zdarzenie: " + event.getData());
    }
}

Połączmy wszystko w klasie głównej aplikacji:

public class Main {
    public static void main(String[] args) {
        DataLoader loader = new DataLoader();
        DataLoadedHandler handler = new DataLoadedHandler();

        // Rejestrujemy słuchacza
        loader.addDataLoadedListener(handler);

        // Uruchamiamy wczytywanie danych
        loader.loadData();
    }
}

Co się wydarzy?

  1. DataLoader wczytuje dane (symulacja).
  2. Po wczytaniu wywołuje fireDataLoaded, który tworzy obiekt zdarzenia i powiadamia wszystkich słuchaczy.
  3. Nasz handler (DataLoadedHandler) otrzymuje zdarzenie i wypisuje komunikat.

Przykładowy wynik:

Dane wczytane: To są wczytane dane!
Handler otrzymał zdarzenie: To są wczytane dane!

Użycie klas anonimowych i wyrażeń lambda

Aby nie mnożyć osobnych klas dla każdego słuchacza, często używa się klas anonimowych lub wyrażeń lambda (od Java 8):

public class Main {
    public static void main(String[] args) {
        DataLoader loader = new DataLoader();

        // Słuchacz poprzez wyrażenie lambda
        loader.addDataLoadedListener(event ->
            System.out.println("Obsługa lambda: " + event.getData())
        );

        loader.loadData();
    }
}

Rejestrowanie i usuwanie słuchaczy

Słuchaczy można dodawać i usuwać. Jest to ważne dla zarządzania pamięcią i zapobiegania wyciekom (zwłaszcza jeśli słuchacz to „ciężki” obiekt lub nie jest już potrzebny).

DataLoadedHandler handler = new DataLoadedHandler();
loader.addDataLoadedListener(handler);

// Później, jeśli handler nie jest już potrzebny:
loader.removeDataLoadedListener(handler);

Jeśli nie usuniemy słuchacza, a samo źródło zdarzenia żyje długo, słuchacz pozostanie na liście i nie zostanie usunięty przez GC — może to prowadzić do wycieków pamięci.

3. Praktyka: mini‑przykład — licznik kliknięć

Zróbmy mały przykład, który można rozwinąć w ramach aplikacji edukacyjnej. Załóżmy, że mamy klasę‑licznik, która zwiększa swoją wartość, a przy każdym zwiększeniu powiadamia słuchaczy o nowej wartości.

Klasa zdarzenia

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;
    }
}

Interfejs słuchacza

import java.util.EventListener;

public interface CounterChangedListener extends EventListener {
    void counterChanged(CounterChangedEvent event);
}

Klasa licznika

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);
        }
    }
}

Użycie

public class Main {
    public static void main(String[] args) {
        Counter counter = new Counter();

        // Rejestrujemy słuchacza przez lambdę
        counter.addCounterChangedListener(event ->
            System.out.println("Licznik się zmienił: " + event.getNewValue())
        );

        counter.increment(); // Licznik się zmienił: 1
        counter.increment(); // Licznik się zmienił: 2
    }
}

4. Typowe błędy przy tworzeniu i obsłudze własnych zdarzeń

Błąd nr 1: zapomniano wywołać metodę powiadamiania słuchaczy. Zdarzenie jest tworzone, ale metoda, która ma powiadomić słuchaczy (fireDataLoaded, fireCounterChanged), nie jest wywoływana. W rezultacie słuchacze „milczą”.

Błąd nr 2: wyjątki w obsłudze słuchaczy. Jeśli jeden ze słuchaczy zgłosi wyjątek, pozostali mogą nie otrzymać powiadomienia. Dobrą praktyką jest opakowywanie wywołań słuchaczy w trycatch, aby jeden „zły” słuchacz nie przeszkadzał pozostałym.

Błąd nr 3: brak usuwania słuchaczy. Jeśli słuchacz nie jest już potrzebny, ale nie został usunięty, nadal będzie otrzymywał zdarzenia i nie zostanie usunięty z pamięci. Może to prowadzić do wycieków pamięci — zwłaszcza gdy źródło żyje długo, a słuchaczy jest wielu.

Błąd nr 4: modyfikacja listy słuchaczy podczas iteracji. Jeśli w obsłudze zdarzenia ktoś dodaje lub usuwa słuchacza, może to prowadzić do ConcurrentModificationException. Bezpieczne podejście to najpierw skopiować listę do osobnej tablicy, a następnie iterować po kopii.

Błąd nr 5: długie operacje w obsłudze. Jeśli handler wykonuje coś czasochłonnego (wczytywanie pliku, oczekiwanie na sieć), interfejs może „zawiesić się”. Przenieś ciężkie zadania do osobnego wątku lub użyj mechanizmów asynchronicznych.

1
Zadanie
JAVA 25 SELF, poziom 50, lekcja 2
Niedostępne
Magiczny impuls komunikacji ✨
Magiczny impuls komunikacji ✨
1
Zadanie
JAVA 25 SELF, poziom 50, lekcja 2
Niedostępne
Magiczny odbiornik impulsów 🔮
Magiczny odbiornik impulsów 🔮
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION