1. Если нужны собственные события
Стандартные события Java вроде нажатия кнопки, движения мыши или изменения текста — это лишь верхушка айсберга. В реальных приложениях возникает масса ситуаций, которые не укладываются в эти рамки. Например, программа может загружать данные из интернета и нужно уведомить другие части приложения, когда загрузка завершена. В игре игрок может получить новую ачивку, и об этом стоит сообщить сразу нескольким компонентам, например интерфейсу и системе логирования. В бизнес-приложении изменение состояния заказа должно триггерить уведомления для бухгалтерии, склада и пользователя одновременно.
Во всех таких случаях полезно создавать собственные типы событий и слушателей. Они позволяют описать уникальные сценарии взаимодействия компонентов и сделать код более гибким и структурированным.
Структура пользовательского события
Чтобы создать своё событие в Java, обычно реализуют три части:
- Класс события — как правило, наследник java.util.EventObject. Хранит сведения о событии (кто источник, какие данные связаны с событием).
- Интерфейс слушателя — например, MyEventListener, который расширяет java.util.EventListener и определяет методы обработки события.
- Механизм подписки/отписки — методы в источнике события для add...Listener/remove...Listener и вызов слушателей при наступлении события (fire...).
Давайте рассмотрим это пошагово на примере приложения, в котором данные загружаются из файла или сети, и нам нужно уведомлять других о завершении загрузки.
Класс события
Создадим класс события, который будет содержать информацию о завершённой загрузке.
import java.util.EventObject;
// Класс события: наследуемся от EventObject
public class DataLoadedEvent extends EventObject {
private final String data; // Дополнительная информация о событии
public DataLoadedEvent(Object source, String data) {
super(source); // source — объект, вызвавший событие
this.data = data;
}
public String getData() {
return data;
}
}
Здесь source — это объект-источник события (например, загрузчик данных), а data — строка с загруженными данными (это может быть путь к файлу, JSON, результат и т.д.).
Интерфейс слушателя
Определим интерфейс слушателя. Обычно он расширяет EventListener (маркерный интерфейс для типизации).
import java.util.EventListener;
// Интерфейс слушателя нашего события
public interface DataLoadedListener extends EventListener {
void dataLoaded(DataLoadedEvent event);
}
Метод dataLoaded будет вызываться, когда событие произойдёт.
Источник события
Нужна сущность, которая будет хранить список слушателей, позволять регистрировать/удалять их и уведомлять при наступлении события.
import java.util.ArrayList;
import java.util.List;
public class DataLoader {
private final List<DataLoadedListener> listeners = new ArrayList<>();
// Регистрация слушателя
public void addDataLoadedListener(DataLoadedListener listener) {
listeners.add(listener);
}
// Удаление слушателя
public void removeDataLoadedListener(DataLoadedListener listener) {
listeners.remove(listener);
}
// Метод, который инициирует событие (например, после загрузки данных)
private void fireDataLoaded(String data) {
DataLoadedEvent event = new DataLoadedEvent(this, data);
// Уведомляем всех слушателей
for (DataLoadedListener listener : listeners) {
listener.dataLoaded(event);
}
}
// Пример метода, который "загружает" данные
public void loadData() {
// Симулируем загрузку (например, из файла или сети)
String loadedData = "Это загруженные данные!";
System.out.println("Данные загружены: " + loadedData);
// Сообщаем всем слушателям
fireDataLoaded(loadedData);
}
}
В реальной жизни метод loadData() может быть асинхронным, читать файл, обращаться к серверу и т.д., но для примера мы просто симулируем загрузку.
2. Использование пользовательского события
Представим, что у нас есть компонент, который хочет узнать о завершении загрузки данных.
public class DataLoadedHandler implements DataLoadedListener {
@Override
public void dataLoaded(DataLoadedEvent event) {
System.out.println("Обработчик получил событие: " + event.getData());
}
}
Свяжем всё вместе в главном классе приложения:
public class Main {
public static void main(String[] args) {
DataLoader loader = new DataLoader();
DataLoadedHandler handler = new DataLoadedHandler();
// Регистрируем слушателя
loader.addDataLoadedListener(handler);
// Запускаем загрузку данных
loader.loadData();
}
}
Что произойдёт?
- DataLoader загружает данные (симуляция).
- После загрузки вызывает fireDataLoaded, который создаёт объект события и уведомляет всех слушателей.
- Наш обработчик (DataLoadedHandler) получает событие и выводит сообщение.
Пример вывода:
Данные загружены: Это загруженные данные!
Обработчик получил событие: Это загруженные данные!
Использование анонимных классов и лямбда-выражений
Чтобы не плодить отдельные классы для каждого слушателя, часто используют анонимные классы или лямбда-выражения (начиная с Java 8):
public class Main {
public static void main(String[] args) {
DataLoader loader = new DataLoader();
// Слушатель через лямбда-выражение
loader.addDataLoadedListener(event ->
System.out.println("Лямбда-обработчик: " + event.getData())
);
loader.loadData();
}
}
Регистрация и удаление слушателей
Слушателей можно добавлять и удалять. Это важно для управления памятью и предотвращения утечек (особенно если слушатель — «тяжёлый» объект или больше не нужен).
DataLoadedHandler handler = new DataLoadedHandler();
loader.addDataLoadedListener(handler);
// Позже, если обработчик больше не нужен:
loader.removeDataLoadedListener(handler);
Если не удалить слушателя, а сам источник события живёт долго, слушатель останется в списке и не будет удалён сборщиком мусора — это может привести к утечке памяти.
3. Практика: мини-пример — счётчик нажатий
Сделаем небольшой пример, который можно развить в рамках учебного приложения. Пусть у нас есть класс-счётчик, который увеличивает своё значение, и при каждом увеличении уведомляет слушателей о новом значении.
Класс события
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;
}
}
Интерфейс слушателя
import java.util.EventListener;
public interface CounterChangedListener extends EventListener {
void counterChanged(CounterChangedEvent event);
}
Класс-счётчик
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);
}
}
}
Использование
public class Main {
public static void main(String[] args) {
Counter counter = new Counter();
// Регистрируем слушателя через лямбду
counter.addCounterChangedListener(event ->
System.out.println("Счётчик изменился: " + event.getNewValue())
);
counter.increment(); // Счётчик изменился: 1
counter.increment(); // Счётчик изменился: 2
}
}
4. Типичные ошибки при создании и обработке собственных событий
Ошибка №1: забыли вызвать метод уведомления слушателей. Событие создаётся, но метод, который должен уведомить слушателей (fireDataLoaded, fireCounterChanged), не вызывается. В результате слушатели «молчат».
Ошибка №2: исключения в обработчиках слушателей. Если один из слушателей выбросит исключение, остальные могут не получить уведомления. Хорошая практика — оборачивать вызовы слушателей в try–catch, чтобы один «плохой» слушатель не мешал остальным.
Ошибка №3: не удаляют слушателей. Если слушатель больше не нужен, но не был удалён, он продолжает получать события и не удаляется из памяти. Это может привести к утечкам памяти — особенно когда источник живёт долго, а слушателей много.
Ошибка №4: модификация списка слушателей во время обхода. Если в обработчике события кто-то добавляет или удаляет слушателя, это может привести к ConcurrentModificationException. Безопасный подход — сначала скопировать список в отдельный массив, затем итерироваться по копии.
Ошибка №5: долгие операции в обработчиках. Если обработчик делает что-то долгое (загрузка файла, ожидание сети), интерфейс может «зависнуть». Переносите тяжёлые задачи в отдельный поток или используйте асинхронные механизмы.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ