События в Java: события и слушатели AWT/Swing

События в Java: события и слушатели AWT/Swing Полезное

Событие (event) в Java — это объект, который описывает изменение состояния источника: нажатие кнопки, выбор пункта списка, ввод символа с клавиатуры. Базовый класс для всех событий — java.util.EventObject, а слушатель (listener) — объект, реагирующий на событие методом обратного вызова.

Здесь разбираем событийную модель GUI-библиотек AWT и Swing (пакет java.awt.event) — клики, нажатия клавиш и другие действия пользователя в окне. Не путайте ее с событиями бизнес-логики в Spring (ApplicationEvent) — та же идея, но для связи компонентов внутри приложения, не для реакции на клик в окне. Разница — в отдельном разделе ниже.

Источник события, объект события, слушатель

В теме легко перепутать четыре термина:

  • Источник события — компонент, который порождает событие: кнопка, текстовое поле, окно.
  • Объект события — экземпляр класса-наследника EventObject; хранит ссылку на источник и детали произошедшего.
  • Слушатель (listener) — объект, реализующий интерфейс-наследник java.util.EventListener; содержит метод, который вызывается при событии.
  • Регистрация — вызов метода addXXXListener(...) на источнике, после которого слушатель начинает получать уведомления.

Сквозная цепочка на одном примере: пользователь кликает по JButton (источник) -> кнопка создает объект ActionEvent (событие) -> Swing вызывает метод actionPerformed() у каждого зарегистрированного ActionListener (слушатель).

Иерархия классов и интерфейсов

java.util.EventObject — корень иерархии, два метода: getSource() (объект-источник) и toString(). java.util.EventListener — маркерный интерфейс без единого метода, от него наследуются все интерфейсы слушателей.

Пакет java.awt.event вводит промежуточный класс AWTEvent (наследник EventObject) и конкретные события поверх него: ActionEvent, MouseEvent, KeyEvent, WindowEvent, ItemEvent и другие. Для каждого типа — свой интерфейс слушателя:

Тип события Интерфейс слушателя Основные методы Типичный источник
ActionEvent ActionListener actionPerformed JButton, JMenuItem
MouseEvent MouseListener mousePressed, mouseReleased, mouseClicked JPanel, любой JComponent
KeyEvent KeyListener keyPressed, keyReleased, keyTyped JTextField, JComponent
WindowEvent WindowListener windowClosing, windowOpened, windowClosed JFrame
ItemEvent ItemListener itemStateChanged JCheckBox, JComboBox

Интерфейс с несколькими методами (например MouseListener) описывает несколько связанных операций — для мыши это нажатие, отпускание, клик, вход и выход курсора. Вызывается ровно тот метод, который соответствует произошедшему.

Регистрация слушателя

Метод регистрации называется по шаблону addXXXListener(XXXListener listener), где XXX — имя события; парный removeXXXListener снимает подписку. Один источник может иметь несколько слушателей одного типа — все они получат уведомление в порядке регистрации.

Полный рабочий пример: окно с тремя кнопками и текстовым полем, один общий слушатель на все кнопки.

package test;

import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JTextField;
import javax.swing.SwingUtilities;

public class TestFrame extends JFrame {
    private static final long serialVersionUID = 1L;
    private JTextField textField;
    private JButton button1, button2, button3;

    public TestFrame() {
        super("Test frame");
        createGUI();
    }

    public void createGUI() {
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        JPanel panel = new JPanel();
        panel.setLayout(new FlowLayout());
        button1 = new JButton("Button 1");
        button1.setActionCommand("Button 1 was pressed!");
        panel.add(button1);
        button2 = new JButton("Button 2");
        button2.setActionCommand("Button 2 was pressed!");
        panel.add(button2);
        button3 = new JButton("Button 3");
        button3.setActionCommand("Button 3 was pressed!");
        panel.add(button3);
        textField = new JTextField();
        textField.setColumns(23);
        panel.add(textField);
        ActionListener actionListener = new TestActionListener();
        button1.addActionListener(actionListener);
        button2.addActionListener(actionListener);
        button3.addActionListener(actionListener);
        getContentPane().add(panel);
        setPreferredSize(new Dimension(320, 100));
    }

    public class TestActionListener implements ActionListener {
        public void actionPerformed(ActionEvent e) {
            textField.setText(e.getActionCommand());
        }
    }

    public static void main(String[] args) {
        SwingUtilities.invokeLater(() -> {
            TestFrame frame = new TestFrame();
            frame.pack();
            frame.setLocationRelativeTo(null);
            frame.setVisible(true);
        });
    }
}

После запуска откроется окно с тремя кнопками и текстовым полем. Клик по любой кнопке подставляет в поле ее actionCommand, например «Button 2 was pressed!». Код в main обязательно выполняется через SwingUtilities.invokeLater: все создание и изменение компонентов Swing должно идти в потоке обработки событий (Event Dispatch Thread), иначе возможны гонки при обновлении интерфейса.

Для интерфейсов с одним методом (ActionListener, ItemListener) начиная с Java 8 удобнее лямбда вместо анонимного класса:

button1.addActionListener(e -> textField.setText(e.getActionCommand()));

Программный вызов события: где легко ошибиться

Событие можно создать и передать слушателю вручную, без клика — иногда так делают в тестах. Здесь легко ошибиться с идентификатором события.

Неверный вариант (компилируется, но id бессмысленный):

public class TestActionListener implements ActionListener {
    public void actionPerformed(ActionEvent e) {
        if (e.getSource() != button3) {
            textField.setText(e.getActionCommand());
        } else {
            ActionEvent e1 = new ActionEvent(button2,
                    Event.MOUSE_DOWN,
                    "Button 2 was pressed programmatically!");
            ActionListener[] listeners = button2.getActionListeners();
            listeners[0].actionPerformed(e1);
        }
    }
}

Код скомпилируется: Event.MOUSE_DOWN — целочисленная константа (501) из старого класса java.awt.Event, а конструктор ActionEvent принимает любой int как id. Но код, который читает e1.getID() и ждет ActionEvent.ACTION_PERFORMED (1001), получит чужое устаревшее значение и может отработать неверно.

Исправление — константа самого ActionEvent:

ActionEvent e1 = new ActionEvent(button2,
        ActionEvent.ACTION_PERFORMED,
        "Button 2 was pressed programmatically!");
ActionListener[] listeners = button2.getActionListeners();
listeners[0].actionPerformed(e1);

После клика по button3 в консоль выведется текст «Button 2 was pressed programmatically!», и это же значение появится в текстовом поле, если в обработчике вызвать textField.setText(e1.getActionCommand()).

Адаптеры: когда слушатель нужен частично

Для интерфейсов с несколькими методами (MouseListener, WindowListener, KeyListener и подобных) в java.awt.event есть классы-адаптеры: MouseAdapter, KeyAdapter, WindowAdapter, FocusAdapter, ComponentAdapter и другие. Каждый адаптер — это абстрактный класс с пустой реализацией всех методов интерфейса.

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

panel.addMouseListener(new MouseAdapter() {
    @Override
    public void mouseClicked(MouseEvent e) {
        System.out.println("Клик по панели: x=" + e.getX() + ", y=" + e.getY());
    }
});

Для интерфейсов с одним методом (ActionListener, ItemListener) адаптер не нужен — там и переопределять больше нечего, лямбда решает ту же задачу короче. Похожий пример для клавиатуры — реакция на Enter через KeyAdapter:

textField.addKeyListener(new KeyAdapter() {
    @Override
    public void keyPressed(KeyEvent e) {
        if (e.getKeyCode() == KeyEvent.VK_ENTER) {
            System.out.println("Enter нажат, текст поля: " + textField.getText());
        }
    }
});

При нажатии Enter программа выведет в консоль текущее содержимое поля. getKeyCode() возвращает код физической клавиши, а не введенный символ (для символа — getKeyChar()).

События AWT/Swing и события Spring — разные механизмы

В Spring есть свой класс ApplicationEvent и интерфейс ApplicationListener — они тоже строятся на java.util.EventObject и java.util.EventListener, но решают другую задачу: не реакцию на клик в окне, а уведомление между компонентами бизнес-логики, например «заказ оформлен».

Критерий AWT/Swing событие Spring ApplicationEvent
Домен Реакция GUI на действие пользователя Уведомление между компонентами бизнес-логики
Источник события Компонент интерфейса (JButton, JFrame) Любой bean, вызвавший publishEvent
Подписка addXXXListener на конкретном компоненте @EventListener на методе или класс, реализующий ApplicationListener

Смешивать их не стоит: у Java-новичка, который встречает слово «событие» в вакансии Spring-разработчика, речь почти всегда про второй случай, а не про клики в GUI.

Выводы

  • Событие Java — объект-наследник EventObject, слушатель — объект-наследник EventListener; связь задается регистрацией addXXXListener.
  • В AWT/Swing под каждый тип события (Action, Mouse, Key, Window, Item) есть свой интерфейс слушателя и метод обратного вызова; адаптеры (MouseAdapter, KeyAdapter и другие) избавляют от реализации ненужных методов.
  • Ручное создание события требует правильного id (константа самого класса события, а не чужого устаревшего класса).
  • Spring ApplicationEvent — другой домен той же идеи: уведомления между компонентами бизнес-логики, а не реакция на действия в окне.

Где применяется / связь с практикой

Событийная модель AWT/Swing — основа любого desktop-приложения на Java с графическим интерфейсом: от учебных калькуляторов до промышленных панелей управления. Принцип «источник -> событие -> подписчик» лежит и в основе Spring-событий, и в паттерне Observer в целом.

Освойте тему на практике

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

Смежные темы: Java concurrency: как работают потоки.

FAQ

Можно ли назначить компоненту несколько слушателей одного типа?
Да, addXXXListener можно вызывать многократно на одном источнике — все слушатели получат уведомление в порядке регистрации.

Что будет, если не зарегистрировать слушатель?
Событие все равно произойдет, но обработать его будет некому: метод actionPerformed не вызовется, и клик не даст видимого эффекта.

Работает ли этот же механизм в консольном приложении без GUI?
Базовые EventObject и EventListener не привязаны к GUI, на их основе можно строить свой Observer в консоли. Но конкретные AWT/Swing события требуют реальных GUI-источников, которых там нет.

OTUS Журнал
Скидка 5% 14-20 сентября на курсы (popup)