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: тривалі операції в обробниках. Якщо обробник виконує тривалі операції (завантаження файлу, очікування мережі), інтерфейс може «зависнути». Переносьте важкі завдання в окремий потік або використовуйте асинхронні механізми.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ