Event-driven programming: listeners, anonymous classes, lambdas
What happens when a button is clicked: the event source, the listener interface, registration, getSource, and writing the handler as an inner class, an anonymous class or a lambda.
The programs you have written so far ran from start to finish in order. A GUI program, on the other hand, waits after opening the window: when the user clicks a button, presses a key or moves the mouse, the related code runs. This is called event-driven programming. In the exam this topic brings “write a handler” questions, converting an anonymous class to a lambda, and compile errors (effectively final, missing public, wrong signature).
Golden rules:
- Event source, event object, listener. When a button is clicked, Swing creates an
ActionEventand calls theactionPerformedmethod of everyActionListenerregistered on the button. - Registration is required. If you write a listener and do not attach it with
addActionListener, nothing happens, and there is no error either. - A lambda works only with a single-method interface. ActionListener yes, MouseListener no.
- A lambda and an anonymous class cannot modify a local variable. Make anything that will change a field.
The event model
| Part | Its job | Example |
|---|---|---|
| Event source | The component that produces the event | JButton, JTextField (Enter), JCheckBox |
| Event object | The object that carries what happened | ActionEvent, ItemEvent, MouseEvent, KeyEvent |
| Event listener | The object that reacts to the event | ActionListener, ItemListener, MouseListener |
| Event handler | The listener’s method that is called | actionPerformed(ActionEvent e) |
The listeners you will use:
| Interface | When? | Method(s) | Registration |
|---|---|---|---|
ActionListener | A button is clicked, Enter in a text field | actionPerformed(ActionEvent) | addActionListener |
ItemListener | A checkbox / radio button is selected or deselected | itemStateChanged(ItemEvent) | addItemListener |
MouseListener | Click, press, release, enter, exit | 5 methods | addMouseListener |
MouseMotionListener | Mouse movement, dragging | 2 methods | addMouseMotionListener |
KeyListener | A key press | 3 methods | addKeyListener |
All of them are in the java.awt.event package: do not forget to write import java.awt.event.*;.
Four ways to write a handler
1. Inner class
A class defined inside another class that is not static. Its biggest advantage: it accesses the outer object’s private fields directly.
public class CounterPanel extends JPanel {
private int count = 0;
private final JButton button = new JButton("Click");
private final JLabel label = new JLabel("Count: 0");
public CounterPanel() {
add(button);
add(label);
button.addActionListener(new ClickHandler()); // registration
}
private class ClickHandler implements ActionListener {
@Override
public void actionPerformed(ActionEvent e) {
count++; // field of the outer class
label.setText("Count: " + count);
}
}
}
After three clicks:
Count: 3
2. The class itself is the listener
The class says implements ActionListener and registers itself with addActionListener(this). Several buttons share the same method; e.getSource() tells which one was clicked.
public class CalcPanel extends JPanel implements ActionListener {
private final JTextField a = new JTextField("8", 4), b = new JTextField("2", 4);
private final JButton plus = new JButton("+"), div = new JButton("/");
private final JLabel result = new JLabel("=");
public CalcPanel() {
add(a); add(b); add(plus); add(div); add(result);
plus.addActionListener(this);
div.addActionListener(this);
}
@Override
public void actionPerformed(ActionEvent e) {
int x = Integer.parseInt(a.getText());
int y = Integer.parseInt(b.getText());
if (e.getSource() == plus) result.setText("= " + (x + y));
else if (e.getSource() == div) result.setText("= " + (x / y));
System.out.println("command=" + e.getActionCommand() + " " + result.getText());
}
}
When + and then / are clicked, and then b is set to “0” and / is clicked again:
command=+ = 10
command=/ = 4
ArithmeticException: / by zero
The handler did not print the last line: the exception that was not caught inside the handler came out, and in the test run the try/catch around the doClick() call caught it. In a real window this exception goes up to Swing’s event thread and a stack trace is printed to the console. That is why you use try/catch inside a handler (see the TemperatureFrame example below).
3. Anonymous inner class
A class with no name, of which a single object is created at the place where it is written. It has the form new InterfaceName() { ...methods... }. You cannot new an interface, but if you add a body, that body becomes a nameless class that implements the interface.
JButton hello = new JButton("Hello");
JLabel msg = new JLabel();
hello.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
msg.setText("Hi, " + e.getActionCommand());
}
});
msg.getText() after a click:
Hi, Hello
4. Lambda expression
An interface with a single abstract method is called a functional interface. ActionListener is one. In that case, instead of the whole template of the anonymous class, you write only the parameter and the body:
hello.addActionListener(e -> msg.setText("Hi, " + e.getActionCommand()));
| Anonymous class | Lambda |
|---|---|
new ActionListener() { | (none) |
public void actionPerformed(ActionEvent e) { | e -> |
msg.setText(...); | msg.setText(...) |
} }); | ); |
If the body has more than one line, braces are used: e -> { line1; line2; }.
Captured variables and this
Anonymous classes and lambdas can use local variables from outside but cannot modify them. The local variable they use must be final or effectively final (never reassigned).
int count = 0;
b.addActionListener(e -> count++); // DOES NOT COMPILE
local variables referenced from a lambda expression must be final or effectively final. Fields (instance variables) are not subject to this rule; use a field for counters, totals and state information.
this means different things in the two:
| Anonymous class | Lambda | |
|---|---|---|
this | The anonymous class’s object | The outer object |
| Access to the outer object | OuterClass.this | Directly this |
Inside Outer.wire(), Outer.toString() = "Outer":
lambda: this → Outer
anonymous: this.toString().equals("Outer") → false
anonymous: Outer.this → Outer
Mouse and adapter classes
MouseListener has five methods. So that you do not have to write all five when you are only interested in clicks, Java provides ready-made adapter classes: MouseAdapter, KeyAdapter, WindowAdapter. They implement all the methods of the interface with empty bodies; you override only what you want. Since an adapter is a class, it is used with an anonymous class, not a lambda.
JPanel area = new JPanel();
area.addMouseListener(new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent e) {
System.out.println("clicked at " + e.getX() + "," + e.getY());
}
});
When the point (40, 25) is clicked:
clicked at 40,25
Checkbox and other events
JCheckBox and JRadioButton produce an ItemEvent when the selection changes. With e.getStateChange() == ItemEvent.SELECTED you learn whether it was checked; or directly with checkBox.isSelected().
JCheckBox bold = new JCheckBox("Bold");
bold.addItemListener(e -> System.out.println("bold=" + (e.getStateChange() == ItemEvent.SELECTED)));
// clicked twice
bold=true
bold=false
Other useful methods:
button.setEnabled(false): the component turns gray and produces no events. In the test run,doClick()on a disabled button did not run the listener.button.removeActionListener(l): removes the registration. For this you need to keep the listener in a variable.textField.addActionListener(...): runs when Enter is pressed in the text field.
PITFALL · the order of several listeners. If you register two listeners on a button, both run. The Java documentation does not guarantee in which order they are called. In the test run (JDK 26) the last registered ran first:
second registered, thenfirst registered. In the exam, do not write code that depends on the order; do the two jobs in order inside a single handler.
Building a window from start to finish
Let’s add events to last week’s TemperatureFrame example. The order: define the components as fields, place them in the constructor, attach the listener at the end.
public class TemperatureFrame extends JFrame {
private final JTextField celsiusField = new JTextField(6);
private final JButton convertButton = new JButton("Convert");
private final JLabel resultLabel = new JLabel("F: ?");
public TemperatureFrame() {
super("C to F");
setLayout(new FlowLayout());
add(new JLabel("Celsius:"));
add(celsiusField);
add(convertButton);
add(resultLabel);
convertButton.addActionListener(e -> {
try {
double c = Double.parseDouble(celsiusField.getText().trim());
double f = c * 9 / 5 + 32;
resultLabel.setText("F: " + f);
} catch (NumberFormatException ex) {
resultLabel.setText("Invalid number");
}
});
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setSize(300, 100);
}
}
When Convert is pressed with the value “100”, then ” 37 ”, then “abc”, the label shows:
F: 212.0
F: 98.6
Invalid number
The lambda parameter is e and the variable in the catch is ex. If you had used the same name (e), you would get a compile error: the lambda parameter is already defined in that scope.
Common mistakes
| Wrong idea | The truth |
|---|---|
| I wrote the listener class, the button works | If you do not register it with addActionListener, nothing happens |
actionPerformed() can be written without a parameter | The signature is actionPerformed(ActionEvent e), and public |
new ActionListener() | It is an interface; add a body (anonymous) or write a lambda |
count++ (local) inside a lambda | Effectively final rule; use a field |
| Every listener can be a lambda | Only single-method interfaces; MouseAdapter for MouseListener |
In an anonymous class this is the outer object | It is the anonymous object; the outer object is Outer.this |
I call actionPerformed myself | Swing calls it, you only register |
e.getSource().equals("+") | getSource returns an object: e.getSource() == plus or getActionCommand() |
Lambda parameter e, and e in the catch too | You cannot have two e in the same scope; use ex in the catch |
Test yourself
Exam-style questions. Solve them on paper first, then reveal the answer.
What is printed after the button is clicked three times?
class CounterPanel extends JPanel {
private int count = 0;
private JButton button = new JButton("Click");
private JLabel label = new JLabel("Count: 0");
CounterPanel() {
add(button); add(label);
button.addActionListener(new ClickHandler());
}
private class ClickHandler implements ActionListener {
public void actionPerformed(ActionEvent e) {
count++;
label.setText("Count: " + count);
}
}
}
// after 3 clicks: System.out.println(label.getText());Show answer
ClickHandler is an inner class; it accesses the outer object's count and label fields directly. On every click actionPerformed runs once: count becomes 1, 2, 3, and the label shows "Count: 3".
Find the compile error.
JButton b = new JButton("+1");
int count = 0;
b.addActionListener(e -> count++);Show answer
Lambdas and anonymous classes can only read local variables from outside, and that variable must be final or effectively final. javac: local variables referenced from a lambda expression must be final or effectively final. Fix: make the counter a field of the class (private int count;); fields are not subject to this rule.
Find the compile error.
class Handler implements ActionListener {
public void actionPerformed() {
System.out.println("clicked");
}
}Show answer
The method in the interface is void actionPerformed(ActionEvent e). The version without parameters becomes a separate method (an overload), and the interface's method stays unimplemented. javac: Handler is not abstract and does not override abstract method actionPerformed(ActionEvent) in ActionListener.
Find the compile error.
JPanel p = new JPanel();
p.addMouseListener(e -> System.out.println("click"));Show answer
A lambda works only with interfaces that have a single abstract method (functional interfaces). MouseListener has five methods (mouseClicked, mousePressed, mouseReleased, mouseEntered, mouseExited). javac: MouseListener is not a functional interface. Fix: new MouseAdapter() { public void mouseClicked(MouseEvent e) { ... } }.
What is printed?
// a = "8", b = "2"; both buttons call this same handler
public void actionPerformed(ActionEvent e) {
int x = Integer.parseInt(a.getText());
int y = Integer.parseInt(b.getText());
if (e.getSource() == plus) result.setText("= " + (x + y));
else if (e.getSource() == div) result.setText("= " + (x / y));
System.out.println("command=" + e.getActionCommand() + " " + result.getText());
}
// user clicks "+" then "/"Show answer
getSource() returns the object that triggered the event, and you tell which button it was with ==. getActionCommand() is by default the text of the button. +: 8 + 2 = 10. /: 8 / 2 = 4 (int division). If b were "0", the / button would throw ArithmeticException: / by zero.
Find the compile error.
JButton b = new JButton("ok");
b.addActionListener(new ActionListener());Show answer
javac: ActionListener is abstract; cannot be instantiated. You either write a class and create that, or add an anonymous class body (new ActionListener() { public void actionPerformed(ActionEvent e) { ... } }), or use a lambda.
Rewrite this anonymous class as a lambda expression.
hello.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
msg.setText("Hi, " + e.getActionCommand());
}
});Show answer
ActionListener has a single method, so a lambda can be written. The parameter list (e), the arrow, the body. Since it is a single expression, no braces or return are needed. When the button is clicked, the label becomes Hi, Hello.
Inside an instance method of class Outer (whose toString() returns "Outer"), what does this refer to (a) inside an anonymous ActionListener and (b) inside a lambda listener?
Show answer
An anonymous class is a new class, and this inside it is the object of that class; for the outer object you write Outer.this. A lambda does not open a new class, and this is the outer object. In the test run, the lambda printed Outer for this, in the anonymous class this.toString().equals("Outer") came out false, and Outer.this printed Outer.
A button's listener is registered, then button.setEnabled(false); is called and button.doClick(); runs. Is the listener called?
Show answer
A disabled component does not produce events; doClick does not click either. In the test run the listener's message was not printed, only the following line ran. This is the standard way to stop a user from repeating an action.
Which statement is correct about the three steps of event handling?
- Create the event object, call actionPerformed yourself, then repaint
- Write a class implementing the listener interface, create an object of it, register it with the source using addXxxListener
- Extend JButton and override its click() method
Show answer
Swing creates the event object and Swing calls actionPerformed; you do not call it. Your job: (1) a class that implements the listener interface (or an anonymous class / lambda), (2) an object of it, (3) registering it with the source using addActionListener. If you forget to register, nothing happens, and there is no error either.
Write the event handling for TemperatureFrame (fields celsiusField, convertButton, resultLabel): when Convert is clicked, read the Celsius value, compute F = C * 9 / 5 + 32 and show F: 212.0. If the text is not a number, show Invalid number. Use a lambda.
Show answer
It is added at the end of the constructor. Accessing fields from inside a lambda is allowed (only local variables must be effectively final). trim() removes whitespace at the edges:
convertButton.addActionListener(e -> {
try {
double c = Double.parseDouble(celsiusField.getText().trim());
double f = c * 9 / 5 + 32;
resultLabel.setText("F: " + f);
} catch (NumberFormatException ex) {
resultLabel.setText("Invalid number");
}
});In the test run: "100" → F: 212.0, " 37 " → F: 98.6, "abc" → Invalid number. Since c is a double, c * 9 / 5 is a double operation; if C were an int, the order of 9 / 5 would matter.Verification: Events were triggered with doClick() in headless mode without opening a window (java -Djava.awt.headless=true verify/SEN1002/W06.java and W06b.java). Compile errors were tried with javac (verify/SEN1002/err/E06a-e.java).