A Sprite that reads an image from a file and implements two interfaces at once, keyboard events delivered by methods the window system calls, and a Timer that replaces the while(true) loop that has driven every animation since Chapter 15. The last lesson of the book. Follows Think Java 2e, Chapter 17 (Advanced Topics), Sections 17.8-17.9, pp. 290-295, cross-referenced against The Java Tutorials — Introduction to Event Listeners.
Subject: Java · 65 slides · code lesson
Open the interactive version of this deck
Title
Think Java 2e · Chapter 17 · Advanced Topics
Sections 17.8-17.9 · pp. 290-295
Objectives
This lesson follows Think Java 2e, Chapter 17 (Advanced Topics), Sections 17.8-17.9, pp. 290-295. Everything on these slides can be checked against those pages.
1. Write a class that implements more than one interface.
2. Read an image from a file and handle the exception that may result.
3. Implement the three KeyListener methods and explain when each is invoked.
4. Use a switch statement with deliberate fall-through to handle two cases identically.
5. Register a listener and give a component the keyboard focus.
6. Replace an animation loop with a Timer and an ActionListener.
Warm-up
Four things, and every one of them is about to be used at once.
Discussion prompt
From Lesson 17b: how many interfaces may a class implement, and what must it then provide? From Lesson 5a: what happens in a switch case with no break? And from Lesson 15b: what does try-catch do?
Hint: As many as needed. And execution falls through.
Answer:
Classes may implement as many interfaces as needed, and must provide every method each one declares. A case with no break falls through into the next one. And try-catch runs the try block, diverting to the catch only for the named exception type.
All three appear in Sprite, which implements two interfaces, catches an IOException, and uses fall-through deliberately. Now that our Drawing is based on Actor instead of DrawablePolygon, we can draw other types of graphics.
Concept
Every program so far has driven itself: a loop you wrote, calling methods you wrote. Event-driven programming inverts that — you write methods and something else decides when to call them.
Figure (svg): Two panels contrasting a program that drives its own loop with one whose methods are called by the framework
Downey & Mayfield, Think Java, 2nd edition (Green Tea Press / O'Reilly, 2020) — Think Java 2e, Chapter 17 (Advanced Topics), Sections 17.8-17.9, pp. 290-295 — Section 17.8 opens on printed page 290.
Section
Section 17.8
Concept
Here is the beginning of a class that reads an image from a file and shows the image moving across the canvas. The class is called Sprite because a moving image is sometimes called a sprite, in the context of computer graphics.
public class Sprite implements Actor, KeyListener {
private int xpos;
private int ypos;
private int dx;
private int dy;
private Image image;
public Sprite(String path, int xpos, int ypos) {
this.xpos = xpos;
this.ypos = ypos;
try {
this.image = ImageIO.read(new File(path));
} catch (IOException exc) {
exc.printStackTrace();
}
}
}| field | holds |
|---|---|
| xpos, ypos | the location of the sprite |
| dx, dy | the velocity in the x and y directions |
| image | the picture, read from a file |
sprite — A computer graphic that may be moved or otherwise manipulated on the screen.
Sprite implements two interfaces: Actor and KeyListener. It extends nothing at all — which is only possible because both of the things it needs to be are interfaces rather than classes.
Notation
The constructor's three lines of file handling repay a close reading.
Annotate
IOException is one the compiler insists on — like InterruptedException in Lesson 15b, you cannot ignore it and still compile.new File(path) does not open anything. A File object is a path; the reading happens in ImageIO.read.printStackTrace is not an empty catch. It reports where the failure happened, which is what you want for a problem you cannot fix at run time.image as null — so drawImage will later be handed a null. Reporting is not recovering.A catch block should do something proportionate to what the exception means. Here the book chooses report and continue, which is honest for a teaching example and would deserve a harder look in real code.
Worked example
Actor requires that we provide draw and step methods.
public void draw(Graphics g) {
g.drawImage(image, xpos, ypos, null);
}
public void step() {
xpos += dx;
ypos += dy;
}| method | does | compare with |
|---|---|---|
| draw(g) | draws the image at the current position | DrawablePolygon's fillPolygon |
| step() | adds the velocity to the position | BlinkingPolygon's counter |
draw places the image.
Why: The draw method draws the image at the sprite's current position.
The fourth argument is null.
Why: g.drawImage(image, xpos, ypos, null) — an optional observer this program does not need.
step moves it.
Why: The step method changes the position based on dx and dy, which are initially zero.
So it starts still.
Why: Until a key press sets dx or dy to something non-zero.
Verify: Notice that Drawing can hold this object even though it has nothing to do with polygons.
Why: Sprite and RegularPolygon share only Object and the Actor interface — one wraps an image file, the other computes vertices with trigonometry. That they can sit in the same list is exactly what Lesson 17b's generalisation was for.
Prediction
Look at the declaration.
public class Sprite implements Actor, KeyListener {| clause | present? |
|---|---|
| extends | ? |
| implements | two interfaces |
Predict first
What is Sprite's superclass?
Correct: java.lang.Object — there is no extends clause
Why: Classes that do not specify a superclass with extends automatically inherit from java.lang.Object — Lesson 14a's rule. Sprite implements two interfaces and extends nothing, which is exactly what interfaces make possible: two obligations, no inherited implementation, and its one extends still unspent.
Concept
Four integers, and the split between them is the standard way to animate anything.
| position | velocity | |
|---|---|---|
| fields | xpos, ypos | dx, dy |
| changed by | step | keyPressed and keyReleased |
| meaning | where it is now | how far it moves per step |
| initially | the constructor arguments | 0 — not moving |
step reads velocity and writes position; the key handlers write velocity and never touch position. That separation is why holding a key produces smooth movement rather than a single jump — the key sets a rate, and the timer applies it.
Trap
Changing position directly on a key press.
public void keyPressed(KeyEvent e) {
switch (e.getKeyCode()) {
case KeyEvent.VK_UP:
ypos -= 5; // moves once per event
break;
...
}
}| what drives the motion | consequence |
|---|---|
| the key-repeat rate | movement stutters and depends on OS settings |
| a brief tap | moves exactly 5 pixels |
| holding the key | a pause, then repeats |
keyPressed is invoked repeatedly while a key is being held down, but at the operating system's key-repeat rate — with a delay before the first repeat. Motion driven by that is visibly uneven.
Set a velocity; let step apply it.
public void keyPressed(KeyEvent e) {
case KeyEvent.VK_UP:
dy = -5; // sets a rate
}
public void step() {
xpos += dx; // applied every frame
ypos += dy;
}| position in keyPressed | velocity plus step | |
|---|---|---|
| driven by | key repeat | the timer, 30 times a second |
| holding a key | stuttering | constant speed |
| releasing it | just stops repeating | keyReleased sets it to 0 |
The values of dx and dy determine how much the sprite moves each time step is invoked. While the user holds down an arrow key, the sprite will move at a constant speed. Input sets intent; the clock sets timing.
Definition probe
Which methods write which fields.
Sort into buckets
Sort each field or action.
Fill the middle
The file might not exist.
Fill in the blanks
try File}(path));
} catch (IOException exc) ___
Why: new File(path) builds a path object without opening anything; ImageIO.read does the reading and may fail. IOException is a checked exception, so Java refuses to compile the call unless it is caught or declared — the same rule that forced the try-catch around Thread.sleep in Lesson 15b.
Socratic
The constructor catches, reports, and returns.
Discussion prompt
If the image file is missing, image stays null and the constructor returns normally. What happens later, and what would be better?
Hint: What does drawImage do with null?
Answer:
The object is left in a broken state. Every later draw call passes a null image, and the sprite simply never appears — with the only clue being a stack trace printed at startup.
Better options: rethrow as an unchecked exception so construction fails loudly, or substitute a placeholder image so the program stays usable.
Catching is not the same as handling. The question to ask is always: after this catch block, is the program still correct? Here it is not, which makes the choice defensible for a book example and wrong for real code.
Section
Section 17.8
Concept
KeyListener is an interface for receiving keyboard events, which means we can detect and respond to key presses. A class that implements KeyListener has to provide the following methods.
void keyPressed(KeyEvent e)
void keyReleased(KeyEvent e)
void keyTyped(KeyEvent e)| method | invoked when |
|---|---|
| keyPressed | a key has been pressed — repeatedly while held down |
| keyReleased | a key has been released, meaning it is no longer down |
| keyTyped | a key has been typed — generally pressed and released |
These methods get invoked when the user presses and releases any key. They take a KeyEvent object as a parameter, which specifies which key was pressed, released, or typed. You never call them; the window system does — which is what event-driven means.
Notation
Pressing and releasing a key produces three separate calls, and knowing which is which matters.
Annotate
Press-and-hold is a state, not an event. The design here uses keyPressed to enter that state and keyReleased to leave it — which is why the sprite moves for exactly as long as the key is down.
Worked example
We can use these methods to design a simple animation using the arrow keys.
public void keyPressed(KeyEvent e) {
switch (e.getKeyCode()) {
case KeyEvent.VK_UP:
dy = -5;
break;
case KeyEvent.VK_DOWN:
dy = +5;
break;
case KeyEvent.VK_LEFT:
dx = -5;
break;
case KeyEvent.VK_RIGHT:
dx = +5;
break;
}
}| key | sets | effect |
|---|---|---|
| VK_UP | dy = −5 | moves up the screen |
| VK_DOWN | dy = +5 | moves down |
| VK_LEFT | dx = −5 | moves left |
| VK_RIGHT | dx = +5 | moves right |
| anything else | nothing | ignored |
Ask the event which key it was.
Why: e.getKeyCode() returns an int, and KeyEvent.VK_UP and friends are the named constants.
Switch on it.
Why: Here's an implementation of keyPressed that uses a switch statement to test which arrow key was pressed and sets dx or dy accordingly.
Up is negative.
Why: Screen coordinates increase downward — the same convention as Lesson 15a's grid.
Ignore everything else.
Why: There is no default branch, so we ignore all other keys.
Verify: Notice VK_UP is a named constant, not a magic number — KeyEvent.VK_UP rather than 38.
Why: Lesson 3a's argument for named constants, in a library. The numeric key codes exist, and nobody should ever type one: the name says what it means and the compiler catches a misspelling.
Prediction
Hold an arrow key down.
void keyPressed(KeyEvent e)
void keyReleased(KeyEvent e)
void keyTyped(KeyEvent e)| action | which method |
|---|---|
| press and hold | ? |
| let go | keyReleased |
Predict first
Which is invoked repeatedly while a key is held?
Correct: keyPressed
Why: This method is invoked repeatedly while a key is being held down. That repetition at the operating system's key-repeat rate is exactly why the sprite sets a velocity here rather than moving — the timing of the repeats is uneven and out of the program's control.
Concept
When the user releases the key, keyReleased sets dx or dy to 0, so the sprite stops moving in that direction.
public void keyReleased(KeyEvent e) {
switch (e.getKeyCode()) {
case KeyEvent.VK_UP:
case KeyEvent.VK_DOWN:
dy = 0;
break;
case KeyEvent.VK_LEFT:
case KeyEvent.VK_RIGHT:
dx = 0;
break;
}
}| case | falls through to | sets |
|---|---|---|
| VK_UP | the VK_DOWN case | dy = 0 |
| VK_DOWN | — | dy = 0 |
| VK_LEFT | the VK_RIGHT case | dx = 0 |
| VK_RIGHT | — | dx = 0 |
The two vertical cases share one body, because releasing either up or down should stop vertical motion. This is Lesson 5a's fall-through used deliberately — two labels on one block, with the break only after the shared code.
Trap
KeyListener declares three, and you must supply three.
public class Sprite implements Actor, KeyListener {
public void keyPressed(KeyEvent e) { ... }
public void keyReleased(KeyEvent e) { ... }
// no keyTyped
}
// error: Sprite is not abstract and does not override
// abstract method keyTyped(KeyEvent) in KeyListener| KeyListener declares | Sprite provides |
|---|---|
| keyPressed | yes |
| keyReleased | yes |
| keyTyped | no — compile error |
We don't need the keyTyped method for this example, but it's required by the interface; if we don't provide one, the compiler will complain. An interface is a complete contract.
Provide it, empty, with a comment saying so.
public void keyTyped(KeyEvent e) {
// do nothing
}| why it is acceptable here | |
|---|---|
| arrow keys type no character | keyTyped never fires for them |
| the sprite ignores other keys anyway | nothing is lost |
| the comment | marks it as a decision, not an oversight |
So we provide an implementation that does nothing. The same judgement as Lesson 15b's empty catch: an empty body is fine when doing nothing is genuinely correct, and the comment is what tells the next reader that you decided rather than forgot.
Prediction
The switch lists only four cases.
switch (e.getKeyCode()) {
case KeyEvent.VK_UP: dy = -5; break;
case KeyEvent.VK_DOWN: dy = +5; break;
case KeyEvent.VK_LEFT: dx = -5; break;
case KeyEvent.VK_RIGHT: dx = +5; break;
}
// the user presses 'q'| key | matches a case? |
|---|---|
| VK_UP | yes |
| 'q' | no |
Predict first
What happens when another key is pressed?
Correct: Nothing — no case matches and there is no default, so the method returns
Why: There is no default branch, so we ignore all other keys. The method is still invoked — the system sends every key event to every registered listener — but the switch matches nothing and the body completes without changing anything.
Fill the middle
Two keys, one shared body.
Fill in the blanks
case KeyEvent.VK_LEFT:
case KeyEvent.VK_RIGHT:
dx = 0;
break;
Why: Left and right both affect horizontal motion, so releasing either sets dx to zero. The first label has no body at all, which makes execution fall through to the second — Lesson 5a's fall-through used deliberately, with the break after the shared code.
Edge cases
Up and right together.
Discussion prompt
The user presses up and then right without releasing either. Work out what dx and dy become, and what happens when only one is released.
Hint: Two separate fields.
Answer:
Both are set: dy = −5 and dx = +5, so the sprite moves diagonally. The two key handlers touch different fields, so neither interferes with the other.
Releasing right calls keyReleased, which sets dx = 0 and leaves dy alone — so the sprite keeps moving up. That is the correct behaviour, and it comes free.
Storing the state in two independent fields is what makes diagonals work without any code for them. A single direction field would have needed eight cases and would still handle release badly.
Section
Section 17.8
Concept
Writing the three methods makes Sprite capable of receiving key events. It does not make anything send them.
Sprite sprite = new Sprite("face-smile.png", 25, 150);
drawing.add(sprite);
drawing.addKeyListener(sprite);
drawing.setFocusable(true);| line | does |
|---|---|
| drawing.add(sprite) | adds it to the list of Actors to draw and step |
| drawing.addKeyListener(sprite) | adds it to the list that will receive key events |
| drawing.setFocusable(true) | gives the canvas the keyboard focus |
Recall that the add method is one that we wrote in Section 17.5. It adds an Actor to the list of objects to be drawn. The second line is a different list entirely — and the same object is on both, which is why it implements two interfaces.
Notation
Three lines and every one of them is necessary. Missing any produces a different silent failure.
Annotate
addKeyListener is inherited from Canvas — Drawing did not write it, and Lesson 17b's extends Canvas is what supplies it.add, the sprite responds to keys and is never drawn.addKeyListener, it is drawn and never moves — the keys go nowhere.Key events are sent to components only when they have the keyboard focus. That is a rule about windows, not about Java — and forgetting it produces a program that works only after you click on it.
Worked example
Each missing line fails silently and differently. Being able to tell them apart is most of debugging event code.
// what is missing if...
// the sprite never appears -> drawing.add
// the sprite appears but never moves -> addKeyListener
// it moves only after you click it -> setFocusable| symptom | cause | fix |
|---|---|---|
| nothing drawn | not in the Actor list | drawing.add(sprite) |
| drawn, ignores keys | not registered as a listener | drawing.addKeyListener(sprite) |
| works after a click | no initial focus | drawing.setFocusable(true) |
| nothing at all happens | no timer started | Section 17.9 |
Ask whether it is drawn.
Why: If not, the problem is the Actor list — nothing to do with events.
Ask whether keys do anything.
Why: If not, the listener registration is missing.
Ask whether clicking first helps.
Why: If it does, the component did not have focus.
Ask whether anything animates.
Why: If nothing moves at all, step is never being called.
Verify: Comment out setFocusable(true), run it, and click the canvas: the sprite starts responding.
Why: That symptom is unmistakable once you have seen it, and baffling before. Event-driven bugs rarely produce errors — they produce silence, so the diagnosis has to come from which part of the behaviour is missing.
Prediction
Key events go somewhere specific.
drawing.setFocusable(true);| detail | |
|---|---|
| key events go to | the component with keyboard focus |
| without this call | ? |
Predict first
What happens if you omit it?
Correct: The canvas may not receive key events until the user clicks it
Why: In graphical applications, key events are sent to components only when they have the keyboard focus. The setFocusable method ensures that drawing will have the focus initially, without the user having to click it first. It is a run-time wiring detail the compiler cannot check.
Concept
Sprite is on two lists because it implements two interfaces, and each list wants a different capability.
| list | wants | calls |
|---|---|---|
| Drawing's Actor list | Actor | draw and step |
| Canvas's listener list | KeyListener | keyPressed, keyReleased, keyTyped |
| what the object is | both | — |
Neither list knows about the other. Drawing's list holds Actors and would be perfectly happy with a polygon; the listener list holds KeyListeners and knows nothing about drawing. The object satisfies both contracts independently — which is the practical payoff of implements as many as needed.
Trap
Implementing KeyListener does not subscribe you to anything.
Sprite sprite = new Sprite("face-smile.png", 25, 150);
drawing.add(sprite);
// no addKeyListener
// the sprite is drawn, and the arrow keys do nothing| done? | |
|---|---|
| implements KeyListener | yes |
| provides the three methods | yes |
| registered with a component | no |
| receives events | no |
**Implementing an interface says I can do this; registering says send them to me.** The compiler checks the first and knows nothing about the second, so this failure is silent.
Implement, then register, then give it focus.
drawing.add(sprite); // to be drawn
drawing.addKeyListener(sprite); // to receive key events
drawing.setFocusable(true); // so key events arrive at all| step | without it |
|---|---|
| implement the interface | will not compile |
| register the listener | compiles, receives nothing |
| make the component focusable | receives nothing until clicked |
The first row is the only one the compiler checks. The other two are run-time wiring — which is the general shape of event-driven code, and why it is worth checking the registrations first when nothing happens.
Definition probe
The same object on two lists.
Sort into buckets
Sort each method by which registration causes it to be called.
Fill the middle
Draw it, listen to it, focus it.
Fill in the blanks
drawing.add(sprite);
drawing.addKeyListener(sprite);
drawing.setFocusable(true);
Why: addKeyListener is inherited from Canvas and subscribes the object to key events; setFocusable makes the canvas eligible to receive them without a click first. Both are run-time wiring, so omitting either compiles perfectly and simply does nothing.
Explain it to yourself
The same object is on both.
Discussion prompt
Drawing could have registered every Actor it holds as a key listener automatically. Why is it better that the two are separate calls?
Hint: Does every drawable thing want keyboard input?
Answer:
Because most drawable things do not want key events. A blinking polygon has no use for them, and forcing every Actor to be a KeyListener would mean three empty methods on every shape.
And the reverse: a key listener need not be drawable. A class handling shortcuts might listen and never appear on screen.
Two capabilities, two interfaces, two registrations. Keeping them independent is what lets a class opt into exactly the ones it wants — and it is the same argument as keeping Actor down to two methods in Lesson 17b.
Section
Section 17.9
Concept
Now that you know about interfaces and events, we can show you a better way to create animations. Previously, we implemented the animation loop by using while (true) and Thread.sleep. Java provides a Timer class (in javax.swing) that encapsulates this behavior.
// The constructor for Timer takes two parameters:
// int delay // milliseconds between events
// ActionListener listener // for handling timer events
Timer timer = new Timer(33, game);
timer.start();| while (true) loop | Timer | |
|---|---|---|
| you write | the loop, the sleep, the catch | one method |
| who drives | your code | the Timer |
| exception handling | InterruptedException, always | none |
| lines | about 8 | 2 |
A Timer is useful for executing code at regular intervals. It is the same idea as while (true) with a sleep — packaged, so that the loop, the delay and the exception handling are the library's problem rather than yours.
Notation
The ActionListener interface requires only one method, actionPerformed.
Annotate
1000 / 30.main returns immediately after timer.start(), and the program keeps running because the window and the timer are still alive.paint: you supply a method, the framework decides when.Three interfaces in two lessons — Actor, KeyListener, ActionListener — and all three exist so that something else can call your code. That is what event-driven programming is.
Worked example
Using a Timer, we can reorganize the code in main by defining a class that implements ActionListener.
public class VideoGame implements ActionListener {
private Drawing drawing;
public VideoGame() {
Sprite sprite = new Sprite("face-smile.png", 50, 50);
drawing = new Drawing(800, 600);
drawing.add(sprite);
drawing.addKeyListener(sprite);
drawing.setFocusable(true);
JFrame frame = new JFrame("Video Game");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.add(drawing);
frame.pack();
frame.setVisible(true);
}
public void actionPerformed(ActionEvent e) {
drawing.step();
}
public static void main(String[] args) {
VideoGame game = new VideoGame();
Timer timer = new Timer(33, game);
timer.start();
}
}| step | what happens |
|---|---|
| new VideoGame() | creates a Sprite, a Drawing and a JFrame |
| new Timer(33, game) | creates a timer that will call game |
| timer.start() | the timer begins firing |
| every 33 ms | actionPerformed runs, calling drawing.step() |
| drawing.step() | steps every Actor, then repaints |
The constructor builds everything.
Why: The main method constructs a VideoGame object, which creates a Sprite, a Drawing, and a JFrame.
Then main creates the timer.
Why: Then it constructs a Timer object and starts the timer.
The timer calls back.
Why: Every 33 milliseconds, the Timer invokes actionPerformed, which invokes step on the Drawing.
Which cascades.
Why: Drawing.step invokes step on all of its Actor objects, which causes them to update their position, color, or other aspects of their appearance. The Drawing.step then repaints the Canvas, and the time step is done.
Verify: Notice there is no loop anywhere in this program.
Why: The while (true) that has driven every animation since Chapter 15 is gone, and nothing replaced it in your code — the loop now lives inside the Timer. That is the whole point of the section.
Prediction
The delay is in milliseconds.
Timer timer = new Timer(33, game);
timer.start();| value | |
|---|---|
| delay | 33 ms |
| 1000 / 33 | ? |
Predict first
Roughly how many times per second?
Correct: About 30
Why: A thousand milliseconds divided by 33 is about 30, which is the standard frame rate for smooth animation — and the same rate Lesson 17b's loop achieved with Thread.sleep(1000 / 30). The Timer takes the delay directly rather than a rate.
Concept
One timer tick reaches every object on the canvas, through four calls that were each written separately.
Figure (svg): A pipeline from the timer through actionPerformed and Drawing step to each actor and a repaint
Each link was written in a different section and none of them knows about the others. The Timer knows only ActionListener; VideoGame knows only Drawing; Drawing knows only Actor. Every join is an interface, which is why the pieces could be built independently.
Trap
The timer's thread is the one drawing your window.
public void actionPerformed(ActionEvent e) {
drawing.step();
loadNextLevelFromDisk(); // takes two seconds
}| consequence | |
|---|---|
| the method takes 2 seconds | no repainting for 2 seconds |
| the window | appears frozen |
| key events | queue up unhandled |
| the next timer tick | delayed |
Everything the window system does is on one thread, including calling your paint, your key handlers, and your actionPerformed. A slow method there stops all of it — which is why an unresponsive window is the classic symptom.
Keep event handlers short.
public void actionPerformed(ActionEvent e) {
drawing.step(); // update and repaint - fast
}| handler | should take |
|---|---|
| actionPerformed | much less than the timer interval |
| keyPressed | essentially no time — it sets two fields |
| paint | as long as drawing takes, and no longer |
actionPerformed here is one line, and keyPressed sets a single field. That is not minimalism for its own sake — an event handler runs on the thread that keeps the interface alive, so short is part of the contract.
Prediction
There is no loop in VideoGame at all.
public static void main(String[] args) {
VideoGame game = new VideoGame();
Timer timer = new Timer(33, game);
timer.start();
}| before | after | |
|---|---|---|
| the repetition | while (true) | ? |
Predict first
Where does the repetition happen now?
Correct: Inside the Timer, which calls actionPerformed at intervals
Why: Java provides a Timer class that encapsulates this behavior. The loop still exists — it is just inside the library rather than in your code, which is why main can return immediately while the animation keeps running.
Definition probe
Some methods you call; some are called on you.
Sort into buckets
Sort each method.
Real world
The while loop worked for three chapters.
Discussion prompt
while (true) with Thread.sleep animated Conway, Langton and the polygons perfectly well. What does a Timer actually improve?
Hint: What else would you want the program to do at the same time?
Answer:
The loop occupies whatever thread runs it. With a Timer, main returns and the program is free to respond to key presses, window resizing and everything else while the animation continues.
And the timer runs on the same thread as the drawing, so an update and a repaint never collide — which a hand-written loop on another thread can do, with results that are very hard to debug.
Plus it is less code and no exception handling. The try/catch around Thread.sleep disappears entirely — a small thing, repeated in every animation you would ever write.
Section
Sections 17.8-17.9 and back
Concept
At this point, you have all of the elements you need to write your own video games. Four callback mechanisms have appeared, and they are all the same shape.
| you provide | declared by | called by | when |
|---|---|---|---|
| paint(Graphics) | Canvas | the window system | the window needs redrawing |
| draw and step | Actor | Drawing | every frame |
| keyPressed and friends | KeyListener | the window system | a key event occurs |
| actionPerformed | ActionListener | Timer | every 33 milliseconds |
In every row you write a method and something else decides when it runs. That inversion is what event-driven means, and interfaces are what make it possible — the caller needs a type it can rely on without knowing your class.
Picture it
Six classes and three interfaces, and no two of them are tightly coupled.
Figure (svg): A UML diagram of VideoGame, Timer, Drawing, Sprite and the three interfaces they implement
Sprite implements two interfaces and extends nothing; VideoGame implements one. Every arrow into an interface is a promise that lets some other class call in without knowing what it is calling.
Worked example
The user is holding the right arrow key. Follow 33 milliseconds all the way through.
// the user holds RIGHT; the timer fires| # | who | calls | effect |
|---|---|---|---|
| 1 | the window system | sprite.keyPressed(e) | dx = +5 |
| 2 | Timer | game.actionPerformed(e) | — |
| 3 | VideoGame | drawing.step() | — |
| 4 | Drawing | sprite.step() | xpos += 5 |
| 5 | Drawing | repaint() | asks for a redraw |
| 6 | the window system | drawing.paint(g) | — |
| 7 | Drawing | sprite.draw(g) | the image appears 5 pixels right |
Two independent sources of calls.
Why: The key event and the timer tick arrive separately and do not know about each other.
The key handler only sets a field.
Why: It does not draw, move or repaint anything.
The timer drives the update.
Why: Which is why the sprite moves at a constant rate regardless of key repeat.
And repaint closes the loop.
Why: You ask; the system calls paint; paint asks each Actor to draw itself.
Verify: Count how many of those seven steps are calls you wrote versus calls made into your code: three in, four out.
Why: Three entry points — keyPressed, actionPerformed, paint — and none of them is called by your program. That ratio is what distinguishes an event-driven program from the console programs of Chapters 1 to 13.
Definition probe
Three interfaces in this program.
Sort into buckets
Sort each method.
Concept
We hope this final chapter has been a helpful summary of topics presented throughout the book.
| topic | chapters | where it reappeared here |
|---|---|---|
| input and output | 3, 9 | reading an image file |
| decisions and loops | 5, 6 | the switch in keyPressed |
| classes and methods | 4, 11 | Sprite and VideoGame |
| arrays and objects | 7, 10, 15 | the ArrayList of Actors |
| inheritance | 14, 16, 17 | DrawablePolygon, and three interfaces |
| graphics | 15, 16, 17 | all of it |
Congratulations on making it to the end! Every technique in this last program was introduced somewhere earlier — the switch statement in Chapter 5, the try-catch in Chapter 15, the interfaces two sections ago. The chapter is a summary as much as a new topic.
Trap
Assuming your handlers run at the same time.
// "keyPressed and actionPerformed are both being called,
// so I need to worry about them interfering"
public void keyPressed(KeyEvent e) {
synchronized (this) { dx = -5; } // unnecessary here
}| assumption | reality |
|---|---|
| handlers run concurrently | they run one at a time |
| you need locking | not for this program |
| what is actually true | one handler finishes before the next starts |
Swing delivers events on a single thread, so keyPressed and actionPerformed never overlap. Adding synchronisation here protects against something that cannot happen.
One thread, one handler at a time — which is why they must be short.
public void keyPressed(KeyEvent e) {
dx = -5; // no locking needed
}
public void actionPerformed(ActionEvent e) {
drawing.step(); // runs only when no handler is running
}| consequence | detail |
|---|---|
| no data races between handlers | they are serialised |
| a slow handler blocks everything | including repainting |
| so keep them short | the real rule |
The single thread is a simplification, not a limitation to work around — it is what makes the earlier hazard the important one. You do not need locks; you do need handlers that finish quickly.
Prediction
main creates a timer, starts it, and ends.
public static void main(String[] args) {
VideoGame game = new VideoGame();
Timer timer = new Timer(33, game);
timer.start();
}| after main returns | still alive? |
|---|---|
| the window | yes |
| the timer | yes |
Predict first
Why does the program keep running?
Correct: The window and the timer keep running on their own threads until the window is closed
Why: timer.start() returns immediately, and so does main — but the graphical interface and the timer continue running. The program ends when the window is closed, which is what setDefaultCloseOperation(EXIT_ON_CLOSE) arranged back in Lesson 15a.
Matching
Four methods you write and never call.
Match the pairs
Why: All four are callbacks — methods you write for something else to invoke. The fourth is the interesting one: Drawing is your own class acting as a framework, calling into objects it knows only through the Actor interface.
Real world
At this point, you have all of the elements you need to write your own video games.
Discussion prompt
You now have shapes, images, keyboard input, a timer and a canvas. What is genuinely missing for a simple game, and which of it do you already know how to build?
Hint: What happens when two things touch?
Answer:
Collision detection is the obvious gap — and Polygon already provides contains and intersects, which Lesson 17a mentioned and never used.
Scoring, levels and state are just fields and conditionals; sound and menus would need new libraries, but the pattern would be the same — an interface, and something that calls it.
The structural work is done. Adding a new kind of object means writing a class that implements Actor, and Drawing needs no change at all — which is the return on the interface, and a fair place to end the book.
Comparison
Fill the blanks.
Comparison matrix
| while (true) loop | Timer | |
|---|---|---|
| who repeats | your code | the Timer |
| the delay | Thread.sleep | the constructor's first argument |
| exception handling | try-catch for InterruptedException | none |
| what you provide | the whole loop | actionPerformed |
| does main return? | no — it runs forever | yes, immediately |
The last row is the practical difference. With a Timer, main finishes and the program is free to respond to everything else — which is why every graphical framework works this way.
Pattern
Implement an interface, register the object, and let the framework call you.
// 1. implement the interface the framework declares
public class Sprite implements Actor, KeyListener {
public void draw(Graphics g) { ... }
public void step() { ... }
public void keyPressed(KeyEvent e) { dx = +5; }
public void keyReleased(KeyEvent e) { dx = 0; }
public void keyTyped(KeyEvent e) { /* do nothing */ }
}
// 2. register the object with whatever sends the events
drawing.add(sprite); // Actor
drawing.addKeyListener(sprite); // KeyListener
drawing.setFocusable(true); // so events arrive
// 3. let something else drive the repetition
Timer timer = new Timer(33, game);
timer.start();| step | checked by the compiler? | symptom if missing |
|---|---|---|
| implement every method | yes | will not compile |
| register the listener | no | silently receives nothing |
| make the component focusable | no | works only after a click |
| start the timer | no | nothing animates |
Check
Work it out before you click.
public void keyPressed(KeyEvent e) {
switch (e.getKeyCode()) {
case KeyEvent.VK_UP:
dy = -5;
break;
...
}
}| detail | |
|---|---|
| when the user holds the key | ? |
Check your understanding
Why does keyPressed set dy rather than changing ypos directly?
Answer: A
Why: keyPressed is invoked repeatedly while a key is being held down — but at the operating system's repeat rate, with a delay before the first repeat, which produces visibly uneven motion. Setting a velocity lets step apply it once per frame: while the user holds down an arrow key, the sprite will move at a constant speed.
Check
Work it out before you click.
Sprite sprite = new Sprite("face-smile.png", 25, 150);
drawing.add(sprite);
drawing.setFocusable(true);
// drawing.addKeyListener(sprite); <- omitted| list | sprite on it? |
|---|---|
| Actor list | yes |
| listener list | no |
Check your understanding
What happens?
Answer: A
Why: Implementing KeyListener says the class can receive key events; addKeyListener is what makes something send them. The compiler checks the first and knows nothing about the second, so the omission compiles perfectly and produces a sprite that is drawn, stepped, and completely unresponsive.
drawing.add is present, so it is on the Actor list and gets drawn.Check
Work it out before you click.
VideoGame game = new VideoGame();
Timer timer = new Timer(33, game);
timer.start();| argument | meaning |
|---|---|
| 33 | ? |
| game | ? |
Check your understanding
What do the two arguments mean?
Answer: A
Why: The constructor for Timer takes two parameters: int delay — milliseconds between events — and ActionListener listener, for handling timer events. 33 milliseconds works out at about 30 frames per second, and game qualifies because VideoGame implements ActionListener. The timer repeats until stopped, so 33 is not a count.
Real world
The structure of this last program is the structure of essentially every interactive application.
Discussion prompt
Web pages, mobile apps and desktop software all use event handlers and callbacks. Why did this pattern win over programs that drive their own loop?
Hint: How many different things can happen at once?
Answer:
Because an interactive program has many possible next events and no way to predict which comes first — a key, a click, a resize, a timer, a network reply. A loop would have to poll for all of them.
Callbacks invert that: you say what should happen for each kind of event, and the system dispatches whichever actually occurs. A web page's onclick is exactly keyPressed by another name.
And the same constraint follows everywhere: handlers run on the thread that keeps the interface responsive, so a slow one freezes the application. That is why don't block the main thread is the first rule of every UI framework ever written.
Commit first
Commit to an answer and to your confidence.
Predict first
Why must Sprite provide a keyTyped method even though it never uses one?
Correct: Because KeyListener declares it, and a concrete class must implement every method of every interface it implements
Why: We don't need the keyTyped method for this example, but it's required by the interface; if we don't provide one, the compiler will complain. So we provide an implementation that does nothing. An interface is all-or-nothing — the same rule as Lesson 16's abstract methods, and the price of the guarantee it gives callers. Arrow keys in fact produce no keyTyped events at all, since they type no character, which is why an empty body here is genuinely correct rather than a shortcut.
Explain it
Two minutes, out loud.
Discussion prompt
A classmate's sprite is drawn on screen but does not move when they press the arrow keys, and there is no error. Walk them through what to check, in order.
Hint: Three things have to be true, and the compiler checks only one.
Answer:
First: does the class implement KeyListener and provide all three methods? If not it would not compile, so if it built, that part is fine.
Second: did you call drawing.addKeyListener(sprite)? Implementing the interface says the object can receive events; registering is what makes something send them, and the compiler cannot check that.
Third: drawing.setFocusable(true) — key events only go to the component with keyboard focus. If clicking on the canvas first makes it work, that is the missing line. And check the timer is started, or nothing moves regardless of the keys.
Exit ticket
One question before you close the deck — and the book.
Predict first
What does it mean that a program is event-driven?
Correct: You write methods and the framework decides when to call them, rather than driving the program with your own loop
Why: Every interface in this chapter exists so that something else can call your code: the window system calls paint and keyPressed, the Timer calls actionPerformed, and Drawing calls draw and step on each Actor. That is the inversion — and it is why the while (true) loop that drove every animation from Chapter 15 onward is absent from the final program. The methods still run on a single thread, one at a time, which is why they have to be short; and main is still there, but its job is now to set things up and get out of the way.
Connect it up
One page, from memory — and then close the book.
Draw it
Draw the seven-step trace of one frame: the key event arriving at the sprite, the timer firing, actionPerformed calling drawing.step, Drawing stepping each Actor, the repaint, and paint calling draw. Mark with an arrow which of the seven are calls into your code. Beside it, list the three interfaces and the methods each requires, and the three wiring lines — add, addKeyListener, setFocusable — with the symptom of omitting each. Finish by writing the one sentence that distinguishes a Timer from a while loop.
Recap
Two sections that hand the driving over to the framework — and the end of Think Java.
| if you remember one thing | it is this |
|---|---|
| about event-driven code | you write methods; the framework calls them |
| about wiring | the compiler checks the interface, not the registration |
| about the whole book | every big program is small pieces with clear contracts |
Want this taught 1-on-1? Alexander tutors Java — $55/session, free consultation.