EventListener: примеры (JAVA)

Примеры и разъяснения по слушателям событий Java
Раздел: События (EventListener)
EventListener

Общее описание интерфейса EventListener и сопутствующие абстракции

В Java термин "слушатель" обычно относится к интерфейсам, расширяющим java.util.EventListener или являющимся самостоятельными функциональными интерфейсами, которые получают события в виде объектов наследников java.util.EventObject или специализированных классов (например, java.awt.event.ActionEvent). Слушатели применяются в GUI (AWT, Swing, JavaFX), в компонентах библиотек, при реализации собственных событийных систем и в инфраструктуре подписки/уведомления.

Основная роль слушателя: получать уведомление о наступлении события и выполнить не возвращающую значение обработку. Общие характеристики:

  • Интерфейс-метка: java.util.EventListener сам по себе не содержит методов. Конкретные интерфейсы добавляют методы с сигнатурами, принимающими объект события и, как правило, возвращающими void.
  • Регистрация и удаление: компоненты обычно предоставляют методы addXListener(XListener l) и removeXListener(XListener l).
  • Тип события: объект события инкапсулирует контекст (источник, координаты, старая/новая величина и т.п.).

Часто используемые интерфейсы и их сигнатуры (кратко):

// java.awt.event
public interface ActionListener extends java.util.EventListener {
    void actionPerformed(ActionEvent e);
}

public interface MouseListener extends java.util.EventListener {
    void mouseClicked(MouseEvent e);
    void mousePressed(MouseEvent e);
    void mouseReleased(MouseEvent e);
    void mouseEntered(MouseEvent e);
    void mouseExited(MouseEvent e);
}

// java.beans
public interface PropertyChangeListener extends java.util.EventListener {
    void propertyChange(PropertyChangeEvent evt);
}

// Общая структура
public class SomeEvent extends EventObject { /* дополнительные поля */ }
public interface SomeListener extends EventListener { void onSomething(SomeEvent e); }

Аргументы и возвращаемые значения:

  • Аргументы: типично один параметр-объект события (наследник EventObject) или более контекстных параметров в специальных API. Методы слушателей обычно принимают объект события и не принимают значения управления потоком.
  • Возвращаемое значение: чаще всего void. Если требуется управление, используются дополнительные методы события (например, consume() в InputEvent) или отдельные контрактные возвращаемые значения в пользовательских системах.

Замечания по применению:

  • В Swing все обновления GUI должны выполняться в EDT; обработчики событий, изменяющие UI, должны запускаться в правильном потоке.
  • Для интерфейсов с множеством методов доступны адаптеры (Adapter classes), которые реализуют все методы пустым телом, чтобы переопределить только нужные.
  • С Java 8 функциональные слушатели (однометодные интерфейсы) могут применяться с лямбда-выражениями.

Короткие примеры использования популярных слушателей

ActionListener - Swing (лямбда)

import javax.swing.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

public class ActionExample {
    public static void main(String[] args) {
        JButton btn = new JButton("Нажми");
        btn.addActionListener((ActionEvent e) -> System.out.println("Кнопка нажата"));
        // Эмуляция вызова обработчика
        btn.doClick();
    }
}
Кнопка нажата

MouseListener - реализация с адаптером

import java.awt.event.*;

public class MouseExample {
    public static void main(String[] args) {
        MouseListener ml = new MouseAdapter() {
            @Override
            public void mouseClicked(MouseEvent e) {
                System.out.println("Клик на координатах: " + e.getX() + "," + e.getY());
            }
        };
        // Эмуляция: создание события и вызов обработчика
        ml.mouseClicked(new MouseEvent(new java.awt.Label(), 0, 0L, 0, 10, 20, 1, false));
    }
}
Клик на координатах: 10,20

Custom listener и EventObject

import java.util.*;

class MyEvent extends EventObject {
    private final String msg;
    public MyEvent(Object src, String msg) { super(src); this.msg = msg; }
    public String getMsg() { return msg; }
}

interface MyListener extends EventListener { void onMyEvent(MyEvent e); }

class Emitter {
    private final List listeners = new ArrayList<>();
    public void addMyListener(MyListener l) { listeners.add(l); }
    public void removeMyListener(MyListener l) { listeners.remove(l); }
    public void fire(String msg) {
        MyEvent e = new MyEvent(this, msg);
        for (MyListener l : listeners) l.onMyEvent(e);
    }
}

public class CustomExample {
    public static void main(String[] args) {
        Emitter em = new Emitter();
        em.addMyListener(e -> System.out.println("Событие: " + e.getMsg()));
        em.fire("Привет");
    }
}
Событие: Привет

Похожие механизмы в Java и их особенности

Несколько альтернативных или близких по назначению решений:

  • PropertyChangeSupport / PropertyChangeListener
  • Упрощает реализацию уведомления об изменениях Java Bean свойств, особенно если нужно уведомлять об старом и новом значении.

  • Observer/Observable
  • Устаревший API (java.util.Observable помечен как deprecated). Не рекомендуется для новых решений; заменяется паттернами событий или реактивными фреймворками.

  • EventListenerList (javax.swing.event.EventListenerList)
  • Удобная структура для хранения множества слушателей и безопасного обхода в Swing-подходе.

  • Событийные шины (EventBus, Guava)
  • Подходящи для крупных приложений, где требуется слабая связанность, асинхронная доставка и фильтрация подписчиков.

  • Reactive Streams / Flow API
  • Современные сценарии с асинхронной потоковой обработкой лучше реализуются через реактивные библиотеки (Reactor, RxJava) или стандартный java.util.concurrent.Flow.

Выбор зависит от требований: простые UI-обработчики - стандартные слушатели; широкодоступные события в приложении - EventBus; асинхронные и backpressure-aware потоки - Reactive Streams.

Аналоги в других языках и их особенности

JavaScript (браузер и Node.js)

// DOM
element.addEventListener('click', () => console.log('click'));

// Node.js
const EventEmitter = require('events');
const e = new EventEmitter();
e.on('msg', m => console.log('got', m));
e.emit('msg','hi');
DOM: вызов при клике
Node.js: got hi

Python

# простой пример с callback
class Emitter:
    def __init__(self):
        self._listeners = []
    def add(self, f):
        self._listeners.append(f)
    def fire(self, v):
        for f in self._listeners:
            f(v)

em = Emitter()
em.add(lambda x: print('got', x))
em.fire('hello')
got hello

В Python часто применяются библиотеки типа blinker для сигналов.

PHP

// Symfony EventDispatcher
use Symfony\Component\EventDispatcher\EventDispatcher;
$disp = new EventDispatcher();
$disp->addListener('app.event', function($e){ echo "ok"; });
$disp->dispatch(new \stdClass(), 'app.event');
ok

C#

public class Publisher {
    public event EventHandler MyEvent;
    protected void Raise() { MyEvent?.Invoke(this, EventArgs.Empty); }
}
Подписчик получает вызов обработчика

В C# события и делегаты встроены в язык, что делает паттерн более безопасным и типизированным.

Go

// В Go чаще используются каналы
ch := make(chan string)
go func(){ for m := range ch { fmt.Println(m) } }()
ch <- "hi"
hi

Kotlin

button.setOnClickListener { println("clicked") }
clicked

Lua

listeners = {}
function add(f) table.insert(listeners,f) end
function fire(v) for _,f in ipairs(listeners) do f(v) end end
add(function(x) print(x) end)
fire('ok')
ok

Отличие от Java: в динамических языках паттерн реализуется через функции первого класса или встроенную инфраструктуру; в C# и Kotlin поддержка событий более «языковая», а в Go концептуально предпочтительнее каналы и горутины.

Типичные ошибки при работе со слушателями

  • Отсутствие регистрации слушателя
  • Код, создающий слушатель, но не добавляющий его к источнику, ничего не вызовет.

    Emitter em = new Emitter();
    em.addMyListener(e -> System.out.println(e.getMsg()));
    // em.fire('x'); // если не вызвать fire, ничего не произойдет
    (нет вывода)
  • Неудаление слушателей и утечки памяти
  • Анонимные слушатели, зарегистрированные в долгоживущих объектах, удерживают ссылки, препятствуя GC.

    component.addPropertyChangeListener(evt -> { /*...*/ });
    // Если component жив долго, а слушатель захватывает внешний контекст, память может не освобождаться
    
    Потенциальная утечка памяти
  • Изменение GUI не в EDT (для Swing)
  • Изменения UI из фоновых потоков приводят к непредсказуемому поведению и багам.

    // Неправильно
    new Thread(() -> button.setText("X")).start();
    // Правильно
    SwingUtilities.invokeLater(() -> button.setText("X"));
    
    Второй вариант безопасен для UI
  • Неполная реализация интерфейса
  • При реализации MouseListener без использования MouseAdapter требуется реализовать все методы или класс сделать абстрактным.

    class M implements MouseListener { // ошибка, если не реализованы все методы }
    
    Ошибка компиляции: класс не реализует абстрактные методы

Изменения и эволюция моделей слушателей в Java

  • Java 8 (лямбды)
  • Однометодные слушатели получили удобную поддержку через лямбда-выражения, что сократило объем шаблонного кода при регистрации обработчиков.

  • Реактивные подходы
  • Появление java.util.concurrent.Flow и популярность Reactor/RxJava сместили часть сценариев от классических слушателей к потокам событий с поддержкой асинхронности и backpressure.

  • Deprecated Observable
  • java.util.Observable считается устаревшим; для широких случаев рекомендуются EventBus или реактивные решения.

Расширенные примеры: слабые слушатели, асинхронная доставка, EventListenerList

WeakReference для предотвращения утечек

Пример java
import java.lang.ref.WeakReference;
import java.util.*;

class WeakEmitter {
    private final List> list = new ArrayList<>();
    public void add(MyListener l) { list.add(new WeakReference<>(l)); }
    public void fire(String msg) {
        Iterator> it = list.iterator();
        while (it.hasNext()) {
            MyListener l = it.next().get();
            if (l == null) it.remove(); else l.onMyEvent(new MyEvent(this,msg));
        }
    }
}

// Результат: слушатель не удерживается GC, очистка списка происходит автоматически
При сборке мусора удаляются слабые ссылки, дальнейшие вызовы fire не будут посылать события мертвым слушателям

Асинхронная доставка через ExecutorService

Пример java
import java.util.concurrent.*;

class AsyncEmitter extends Emitter {
    private final ExecutorService ex = Executors.newFixedThreadPool(2);
    @Override
    public void fire(String msg) {
        for (MyListener l : new ArrayList<>(listeners)) {
            ex.submit(() -> l.onMyEvent(new MyEvent(this, msg)));
        }
    }
}

// Результат: обработчики выполняются в потоках из пула, исходный поток не блокируется
События доставляются параллельно в фоновых потоках

Использование EventListenerList в Swing-компонентах

Пример java
import javax.swing.event.EventListenerList;

class Comp {
    protected EventListenerList listenerList = new EventListenerList();
    public void addMyListener(MyListener l){ listenerList.add(MyListener.class, l); }
    public void removeMyListener(MyListener l){ listenerList.remove(MyListener.class, l); }
    protected void fireMy(String msg){
        Object[] ls = listenerList.getListenerList();
        for (int i = ls.length-2; i >= 0; i -= 2) {
            if (ls[i] == MyListener.class) ((MyListener)ls[i+1]).onMyEvent(new MyEvent(this,msg));
        }
    }
}

// Результат: компактное хранение и быстрый обход всех зарегистрированных слушателей
Все добавленные слушатели получают вызов fireMy

Фильтрация и приоритеты

Можно реализовать промежуточный диспетчер, который будет фильтровать события по типу или приоритету и вызывать подмножество подписчиков. Такой подход полезен в больших системах с множеством подписчиков.

Пример java
// Пример концепции: фильтр по категории
interface FilteredListener extends MyListener { String category(); }
// Эмиттер хранит карту категорий и вызывает только совпадающих подписчиков
Позволяет доставлять события только заинтересованным обработчикам

Динамическая генерация прокси-слушателей

Для сложных случаев можно создавать динамические прокси (java.lang.reflect.Proxy) или использовать AOP для внедрения логики вокруг вызовов слушателей - полезно для логирования, метрик и обработки исключений.

Пример java
// Концепция: Proxy.newProxyInstance для интерфейса слушателя
При вызове метода прокси можно добавить try/catch, таймауты или трассировку

джава EventListener function comments

En
EventListener A tagging interface that all event listener interfaces must extend