EventListener: примеры (JAVA)
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
- Observer/Observable
- EventListenerList (javax.swing.event.EventListenerList)
- Событийные шины (EventBus, Guava)
- Reactive Streams / Flow API
Упрощает реализацию уведомления об изменениях Java Bean свойств, особенно если нужно уведомлять об старом и новом значении.
Устаревший API (java.util.Observable помечен как deprecated). Не рекомендуется для новых решений; заменяется паттернами событий или реактивными фреймворками.
Удобная структура для хранения множества слушателей и безопасного обхода в Swing-подходе.
Подходящи для крупных приложений, где требуется слабая связанность, асинхронная доставка и фильтрация подписчиков.
Современные сценарии с асинхронной потоковой обработкой лучше реализуются через реактивные библиотеки (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 жив долго, а слушатель захватывает внешний контекст, память может не освобождаться
Потенциальная утечка памяти
Изменения UI из фоновых потоков приводят к непредсказуемому поведению и багам.
// Неправильно
new Thread(() -> button.setText("X")).start();
// Правильно
SwingUtilities.invokeLater(() -> button.setText("X"));
Второй вариант безопасен для UI
При реализации MouseListener без использования MouseAdapter требуется реализовать все методы или класс сделать абстрактным.
class M implements MouseListener { // ошибка, если не реализованы все методы }
Ошибка компиляции: класс не реализует абстрактные методы
Изменения и эволюция моделей слушателей в Java
- Java 8 (лямбды)
- Реактивные подходы
- Deprecated Observable
Однометодные слушатели получили удобную поддержку через лямбда-выражения, что сократило объем шаблонного кода при регистрации обработчиков.
Появление java.util.concurrent.Flow и популярность Reactor/RxJava сместили часть сценариев от классических слушателей к потокам событий с поддержкой асинхронности и backpressure.
java.util.Observable считается устаревшим; для широких случаев рекомендуются EventBus или реактивные решения.
Расширенные примеры: слабые слушатели, асинхронная доставка, EventListenerList
WeakReference для предотвращения утечек
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
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-компонентах
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
Фильтрация и приоритеты
Можно реализовать промежуточный диспетчер, который будет фильтровать события по типу или приоритету и вызывать подмножество подписчиков. Такой подход полезен в больших системах с множеством подписчиков.
// Пример концепции: фильтр по категории
interface FilteredListener extends MyListener { String category(); }
// Эмиттер хранит карту категорий и вызывает только совпадающих подписчиков
Позволяет доставлять события только заинтересованным обработчикам
Динамическая генерация прокси-слушателей
Для сложных случаев можно создавать динамические прокси (java.lang.reflect.Proxy) или использовать AOP для внедрения логики вокруг вызовов слушателей - полезно для логирования, метрик и обработки исключений.
// Концепция: Proxy.newProxyInstance для интерфейса слушателя
При вызове метода прокси можно добавить try/catch, таймауты или трассировку