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:
- Klasa zdarzenia — zazwyczaj potomna java.util.EventObject. Przechowuje informacje o zdarzeniu (kto jest źródłem, jakie dane są z nim związane).
- Interfejs słuchacza — na przykład MyEventListener, który rozszerza java.util.EventListener i definiuje metody obsługi zdarzenia.
- 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?
- DataLoader wczytuje dane (symulacja).
- Po wczytaniu wywołuje fireDataLoaded, który tworzy obiekt zdarzenia i powiadamia wszystkich słuchaczy.
- 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 try–catch, 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.
GO TO FULL VERSION