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: долгие операции в обработчиках. Если обработчик делает что-то долгое (загрузка файла, ожидание сети), интерфейс может «зависнуть». Переносите тяжёлые задачи в отдельный поток или используйте асинхронные механизмы.

1
Задача
JAVA 25 SELF, 50 уровень, 2 лекция
Недоступна
Магический импульс связи ✨
Магический импульс связи ✨
1
Задача
JAVA 25 SELF, 50 уровень, 2 лекция
Недоступна
Магический приемник импульсов 🔮
Магический приемник импульсов 🔮
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ