JavaRush /Курси /JAVA 25 SELF /Створення та обробка власних подій

Створення та обробка власних подій

JAVA 25 SELF
Рівень 50 , Лекція 2
Відкрита

1. Коли потрібні власні події

Стандартні події Java на кшталт натискання кнопки, руху миші або зміни тексту — це лише верхівка айсберга. У реальних застосунках виникає безліч ситуацій, які не вписуються в ці рамки. Наприклад, програма може завантажувати дані з інтернету й потрібно сповістити інші частини застосунку, коли завантаження завершено. У грі гравець може отримати нове досягнення, і про це варто повідомити відразу кілька компонентів, наприклад інтерфейс і систему логування. У бізнес‑застосунку зміна стану замовлення має ініціювати сповіщення для бухгалтерії, складу й користувача одночасно.

У всіх таких випадках корисно створювати власні типи подій і слухачів. Вони дають змогу описати унікальні сценарії взаємодії компонентів і зробити код гнучкішим і структурованішим.

Структура власної події

Щоб створити власну подію в Java, зазвичай реалізують три частини:

  1. Клас події — зазвичай нащадок java.util.EventObject. Зберігає відомості про подію (хто є джерелом, які дані пов’язані з подією).
  2. Інтерфейс слухача — наприклад, MyEventListener, який розширює java.util.EventListener і визначає методи обробки події.
  3. Механізм підписки/відписки — методи в джерелі події для 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();
    }
}

Що відбудеться?

  1. DataLoader завантажує дані (імітація).
  2. Після завантаження викликає fireDataLoaded, який створює об’єкт події та сповіщає всіх слухачів.
  3. Наш обробник (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: винятки в обробниках слухачів. Якщо один зі слухачів кине виняток, інші можуть не отримати сповіщення. Хороша практика — обгортати виклики слухачів у trycatch, щоб один «поганий» слухач не заважав іншим.

Помилка № 3: не видаляють слухачів. Якщо слухач більше не потрібен, але не був видалений, він продовжує отримувати події й не видаляється з пам’яті. Це може призвести до витоків пам’яті — особливо коли джерело живе довго, а слухачів багато.

Помилка № 4: модифікація списку слухачів під час обходу. Якщо в обробнику події хтось додає або видаляє слухача, це може призвести до ConcurrentModificationException. Безпечний підхід — спочатку скопіювати список в окремий масив, потім перебирати копію.

Помилка № 5: тривалі операції в обробниках. Якщо обробник виконує тривалі операції (завантаження файлу, очікування мережі), інтерфейс може «зависнути». Переносьте важкі завдання в окремий потік або використовуйте асинхронні механізми.

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ