A second zero-player game — Langton's Ant — reusing Cell and GridCanvas unchanged, which leaves two copies of main and mainloop. Removing that duplication introduces refactoring, and then `protected`, `abstract` and the distinction between an abstract and a concrete class. Follows Think Java 2e, Chapter 16 (Reusing Classes), Sections 16.1-16.4, pp. 267-274, cross-referenced against The Java Tutorials — Abstract Methods and Classes.
Subject: Java · 65 slides · code lesson
Open the interactive version of this deck
Title
Think Java 2e · Chapter 16 · Reusing Classes
Sections 16.1-16.4 · pp. 267-274
Objectives
This lesson follows Think Java 2e, Chapter 16 (Reusing Classes), Sections 16.1-16.4, pp. 267-274. Everything on these slides can be checked against those pages.
1. Implement Langton's Ant using the Cell and GridCanvas classes unchanged.
2. Use the remainder operator to make a direction wrap around, and explain why turning left adds 3.
3. Define refactoring and recognise when repeated code calls for it.
4. Extract a superclass containing the code two classes have in common.
5. Explain what protected, abstract class and abstract method each mean.
6. Read a UML diagram that shows inheritance, composition, use, and an abstract class.
Warm-up
Four things, all of which this chapter reuses without ceremony.
Discussion prompt
From Lesson 3b: what does % compute, and what is -1 % 4 in Java? From Lesson 14a: what does a subclass inherit, and what does private keep out? And from Lesson 15b: what is the shape of a simulation loop?
Hint: A remainder with a sign. And private excludes more than you might think.
Answer:
% gives the remainder, and -1 % 4 is -1 in Java — the sign follows the left operand. A subclass inherits every public member but not constructors, and private excludes subclasses too. A simulation loop is update, repaint, sleep.
All four appear in the first two pages. We can reuse the Cell and GridCanvas classes to implement other simulations — and the fact that we do not have to modify them at all is the chapter's opening claim.
Concept
One of the most interesting zero-player games is Langton's Ant, which models an ant that walks around a grid. The ant follows only two simple rules.
Figure (svg): Two panels stating the two rules of Langton's Ant for a white cell and a black cell
Downey & Mayfield, Think Java, 2nd edition (Green Tea Press / O'Reilly, 2020) — Think Java 2e, Chapter 16 (Reusing Classes), Sections 16.1-16.4, pp. 267-274 — Chapter 16 opens on printed page 267.
Section
Section 16.1
Concept
Because the rules are simple, you might expect the ant to do something simple, like make a square or repeat a simple pattern.
// 1. If the ant is on a WHITE cell, it turns to the RIGHT,
// makes the cell black, and moves forward.
//
// 2. If the ant is on a BLACK cell, it turns to the LEFT,
// makes the cell white, and moves forward.| phase | roughly how many steps | what it looks like |
|---|---|---|
| early | the first few hundred | small symmetric patterns |
| chaotic | up to about 10,000 | seemingly random |
| the highway | a repeating loop of 104 steps | a diagonal road, forever |
But starting on a grid with all white cells, the ant makes more than 10,000 steps in a seemingly random pattern before it settles into a repeating loop of 104 steps. Two rules, and nobody can predict that from reading them — the same lesson as the Game of Life, from a completely different mechanism.
Picture it
One ant, at the middle of a white grid, facing north. Everything else follows.
Figure (svg): A grid with a single ant marker at the centre facing north on an otherwise empty board
There is no randomness anywhere in this program. The same starting state always produces the same 10,000 steps — which makes seemingly random exactly the right phrase.
Worked example
We begin by defining a Langton class that has a grid and information about the ant.
public class Langton {
private GridCanvas grid;
private int xpos;
private int ypos;
private int head; // 0=North, 1=East, 2=South, 3=West
public Langton(int rows, int cols) {
grid = new GridCanvas(rows, cols, 10);
xpos = rows / 2;
ypos = cols / 2;
head = 0;
}
}| field | holds | changes? |
|---|---|---|
| grid | a GridCanvas — the state of the cells | its cells do |
| xpos | the ant's column | every step |
| ypos | the ant's row | every step |
| head | which way it faces: 0 to 3 | every step |
The grid is reused unchanged.
Why: grid is a GridCanvas object, which represents the state of the cells.
Two coordinates track the ant.
Why: xpos and ypos are the coordinates of the ant.
A fourth field holds the direction.
Why: head is the heading of the ant; that is, which direction it is facing.
The heading is encoded as an integer.
Why: head is an integer with four possible values, where 0 means the ant is facing north (i.e., toward the top of the screen), 1 means east, etc.
Verify: Notice that this class contains no Cell code and no drawing code at all.
Why: Because we designed Cell and GridCanvas to be reusable, we didn't have to modify them at all. That claim is the whole reason the chapter opens with a second game — it is the test of Lesson 15a's design, and it passes.
Prediction
Four values, one per direction.
private int head; // 0=North, 1=East, 2=South, 3=West| head | direction |
|---|---|
| 0 | ? |
| 1 | east |
Predict first
Which way is the ant facing?
Correct: North — toward the top of the screen
Why: 0 means the ant is facing north, i.e. toward the top of the screen. The constructor sets head = 0, so every run begins facing north — and the four values run clockwise, which is what makes turn right the same as add one.
Concept
head is Lesson 12a's encoding argument in a fourth setting: four things represented by four integers, so that arithmetic can act on them.
| head | direction | moving forward means |
|---|---|---|
| 0 | north | ypos − 1 |
| 1 | east | xpos + 1 |
| 2 | south | ypos + 1 |
| 3 | west | xpos − 1 |
The order is deliberate: clockwise. North, east, south, west — so turn right is add one, which is the arithmetic the next section depends on. Choosing 0 = north and going clockwise is a design decision that makes one line of code possible.
Trap
The ant's coordinates are used as (row, column) — and named x and y.
xpos = rows / 2; // xpos from ROWS
ypos = cols / 2; // ypos from COLS
Cell cell = grid.getCell(xpos, ypos); // getCell(r, c)| getCell expects | the call passes | |
|---|---|---|
| first argument | a row index | xpos |
| second argument | a column index | ypos |
| so xpos is really | — | the row |
The names say x and y; the use says row and column. On a square grid nothing breaks — which is exactly why the mismatch survives. Change to a 40×80 grid and the ant starts somewhere unexpected.
Read the code, not the names, and keep the convention consistent.
// grid.getCell(r, c) takes ROW first (Lesson 15a)
// so in this class, xpos is the row index and ypos the column
// clearer, if you were writing it fresh:
private int row;
private int col;| habit | why |
|---|---|
| name grid indexes r and c | so a swap looks wrong |
| name pixel coordinates x and y | so the two never blur |
| test on a non-square grid | it is the only case that reveals a swap |
The book's own moveAnt treats ypos as the vertical axis — north decreases ypos — which is consistent with y-as-row. The names are the weak point, not the logic, and Lesson 15a's advice to call them r and c is exactly the fix.
Definition probe
Cell and GridCanvas were not modified.
Sort into buckets
Sort each responsibility.
Fill the middle
Start in the middle, facing north.
Fill in the blanks
public Langton(int rows, int cols) 2};
ypos = cols / 2;
head = 0;
}
Why: Integer division puts the ant at the middle of the grid, and 0 is the encoding for north. Starting in the centre matters: the ant wanders far before settling, so a 61×61 grid is about the smallest that shows the highway forming.
Socratic
Two rules, ten thousand unpredictable steps.
Discussion prompt
Langton's Ant and the Game of Life both have a handful of rules and behaviour nobody can predict from reading them. What do they have in common that produces that?
Hint: What does each step depend on?
Answer:
Each step depends on the state left by earlier steps. The ant flips the cell it stands on, so it keeps walking back into a world it has already changed.
That feedback is what makes the behaviour impossible to summarise: the only way to know what step 10,000 looks like is to run the first 9,999.
Both are deterministic and both are unpredictable, which is not a contradiction — it is the whole point of a cellular automaton, and it is why these two tiny programs are famous.
Section
Section 16.1
Concept
The flipCell method gets the Cell at the ant's location, figures out which way to turn, and changes the state of the cell.
private void flipCell() {
Cell cell = grid.getCell(xpos, ypos);
if (cell.isOff()) {
head = (head + 1) % 4; // turn right
cell.turnOn();
} else {
head = (head + 3) % 4; // turn left
cell.turnOff();
}
}| cell | turn | new head from 0 | cell becomes |
|---|---|---|---|
| off — white | right | (0 + 1) % 4 = 1 | on |
| on — black | left | (0 + 3) % 4 = 3 | off |
Both rules have the same three parts — turn, flip the cell, move forward — so flipCell does the first two and moveAnt does the third. The if splits on the cell's colour exactly as the rules do.
Notation
The obvious way to turn left is to subtract one. Java makes that a bug.
Annotate
(head + 1) % 4 wraps 3 back to 0 — the remainder operator turns a straight line into a circle.-1 % 4 is -1, not 3. Java's remainder takes the sign of the left operand, which Lesson 3b introduced and this is the first place it bites.moveAnt's chain of ifs would fall through to the else — the ant would move west when it should move west... by accident.+1 nor +3 would mean anything.(x + n) % k is the standard way to step around a cycle, and keeping the addend positive is the standard way to avoid Java's negative remainder. Worth remembering as one idiom.
Worked example
The moveAnt method moves the ant forward one square, using head to determine which way is forward.
private void moveAnt() {
if (head == 0) {
ypos -= 1;
} else if (head == 1) {
xpos += 1;
} else if (head == 2) {
ypos += 1;
} else {
xpos -= 1;
}
}| head | direction | change |
|---|---|---|
| 0 | north | ypos − 1 — up the screen |
| 1 | east | xpos + 1 |
| 2 | south | ypos + 1 — down the screen |
| 3 | west | xpos − 1 |
Four cases, one per heading.
Why: A chain of else-ifs, from Lesson 5a.
North decreases ypos.
Why: Screen coordinates count downward — row 0 is at the top.
The last case needs no test.
Why: else catches head == 3, since head is always 0 to 3.
Each case changes exactly one coordinate.
Why: Which is why the ant moves orthogonally and never diagonally.
Verify: Start at (30, 30) facing north, and confirm one step puts the ant at ypos 29.
Why: The else is load-bearing. It works only because flipCell guarantees head stays in 0..3 — and that guarantee comes entirely from the % 4. Remove the modulo and the else silently absorbs every out-of-range value.
Prediction
head is 3; the ant is on a white cell.
head = (head + 1) % 4;| head | head + 1 | % 4 |
|---|---|---|
| 3 | 4 | ? |
Predict first
What is the new head?
Correct: 0 — north
Why: If head is 3 and we turn right, it wraps around to 0. That is what the remainder operator is for here: it turns the range 0..3 into a circle, so the fourth right turn from north gets back to north.
Concept
Langton's update is two lines, and notably shorter than Conway's.
public void update() {
flipCell();
moveAnt();
}| Conway.update | Langton.update | |
|---|---|---|
| cells changed per step | potentially all of them | exactly one |
| needs a snapshot? | yes — a counts array | no |
| reads neighbours? | yes, eight of them | no |
| lines | about 30 with its helpers | 2, plus two helpers |
Langton's Ant needs no two-pass update because only the cell under the ant changes, and nothing reads a neighbour. Lesson 15b's simultaneity problem simply does not arise — which is a useful reminder that the two-pass structure was solving a specific problem, not a general one.
Trap
(head - 1) % 4 goes negative.
head = (head - 1) % 4; // turn left - WRONG
// with head == 0:
// (0 - 1) % 4 -> -1 % 4 -> -1 in Java| head | expression | result | valid? |
|---|---|---|---|
| 3 | (3 − 1) % 4 | 2 | yes |
| 1 | (1 − 1) % 4 | 0 | yes |
| 0 | (0 − 1) % 4 | −1 | no |
It works for three of the four values, which is the worst possible failure mode. The bug appears only when an ant on a black cell happens to be facing north — and then the heading becomes −1 and stays wrong forever.
Add three instead, and stay non-negative.
head = (head + 3) % 4; // turn left| head | (head + 3) % 4 | means |
|---|---|---|
| 0 — north | 3 | west — correct left turn |
| 1 — east | 0 | north |
| 2 — south | 1 | east |
| 3 — west | 2 | south |
Since one left turn is the same as three right turns, the two are equivalent modulo 4 — and only one of them stays positive. When stepping backward around a cycle in Java, add (k − 1) rather than subtracting 1.
Prediction
The sign follows the left operand.
System.out.println(-1 % 4);| expression | value |
|---|---|
| 7 % 4 | 3 |
| -1 % 4 | ? |
Predict first
What is printed?
Correct: -1
Why: -1 % 4 is -1 in Java — the remainder takes the sign of the dividend, unlike the mathematical modulo, which would give 3. That single fact is why flipCell adds 3 to turn left rather than subtracting 1.
Fill the middle
Right adds one; left adds three.
Fill in the blanks
if (cell.isOff()) 1}) % 4; // turn right
cell.turnOn();
} else 3}) % 4; // turn left
cell.turnOff();
}
Why: Turning right steps forward one position in the clockwise encoding; turning left is the same as three right turns, which is used instead of subtracting 1 because -1 % 4 is -1 in Java. Both stay in range because of the % 4.
Edge cases
moveAnt ends with a bare else.
Discussion prompt
moveAnt tests head == 0, 1, 2 and then uses else for the rest. What does that assume, and what would a head of 7 or −1 do?
Hint: Which branch catches everything unlisted?
Answer:
It assumes head is always 0, 1, 2 or 3 — a guarantee that comes entirely from the % 4 in flipCell.
A head of 7 or −1 would fall into the else and move the ant west, silently. No error, no message — just an ant walking the wrong way.
An else that catches one intended case also catches every unintended one. Writing else if (head == 3) with a final else that reports an error would be more defensive — a trade between brevity and a bug that announces itself.
Section
Section 16.2
Concept
And that's everything! Langton's Ant works, Cell and GridCanvas were not touched — however, we now have two copies of main and mainloop, one in Conway, and one in Langton.
public static void main(String[] args) {
String title = "Langton's Ant";
Langton game = new Langton(61, 61);
JFrame frame = new JFrame(title);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setResizable(false);
frame.add(game.grid);
frame.pack();
frame.setVisible(true);
game.mainloop();
}| line | same as Conway's? |
|---|---|
| the title string | no |
| which game object is created | no |
| the JFrame construction and configuration | yes, identical |
| add, pack, setVisible | yes |
| mainloop() | yes |
Most of this code is the same as the main we used to create and run Conway, in Section 15.6. Two lines differ and seven do not — which is the shape that Chapter 14 taught you to recognise.
Notation
The chapter names the technique it has just used twice.
Annotate
Refactor: to restructure or reorganize existing source code without changing its behavior. Six words of the definition are without changing its behavior, and they are the important ones.
Worked example
First, we define a superclass named Automaton, in which we will put the code that Conway and Langton have in common.
public class Automaton {
private GridCanvas grid;
public void run(String title, int rate) {
JFrame frame = new JFrame(title);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setResizable(false);
frame.add(this.grid);
frame.pack();
frame.setVisible(true);
this.mainloop(rate);
}
}| moved into Automaton | was in |
|---|---|
| the grid field | both Conway and Langton |
| JFrame construction and configuration | both mains |
| add, pack, setVisible | both mains |
| the call to mainloop | both mains |
Declare the shared field.
Why: Automaton declares grid as an instance variable, so every Automaton has a GridCanvas.
Move the shared code into a method.
Why: It also provides run, which contains the code that creates and configures the JFrame.
Parameterise what differed.
Why: The run method takes two parameters: the window title and the frame rate.
Pass one straight through.
Why: It uses title when creating the JFrame, and it passes rate to mainloop.
Verify: Compare the two original mains and check that title and the game object are the only differences.
Why: The parameters are exactly the things that differed. That is the general recipe: what is the same becomes the method body; what differs becomes a parameter — Lesson 5b's generalisation, applied to two whole methods rather than one expression.
Prediction
The rate is frames per second.
Thread.sleep(1000 / rate);| rate | 1000 / rate |
|---|---|
| 2 | 500 |
| 4 | ? |
Predict first
How long does each pause last?
Correct: 250 milliseconds
Why: Four frames per second means a quarter of a second between them, and 1000 / 4 is 250. Taking the rate as the parameter rather than the delay means the caller says what they want — run(title, 4) — instead of computing a millisecond count.
Concept
mainloop contains the code you first saw in Section 15.7, with the hard-coded pause replaced by a computed one.
private void mainloop(int rate) {
while (true) {
// update the drawing
this.update();
grid.repaint();
// delay the simulation
try {
Thread.sleep(1000 / rate);
} catch (InterruptedException e) {
// do nothing
}
}
}| rate | 1000 / rate | frames per second |
|---|---|---|
| 1 | 1000 ms | 1 |
| 2 | 500 ms | 2 |
| 10 | 100 ms | 10 |
| 1000 | 1 ms | 1000 — as fast as it can draw |
For example, if rate is 2, we should draw two frames per second, so the delay is a half second, or 500 milliseconds. The parameter is the rate rather than the delay, which is the more natural thing for a caller to specify — run(title, 2) says two frames a second without any arithmetic at the call site.
Trap
Tidying and improving in the same edit.
// while moving main into Automaton, also:
// - change the frame rate from 2 to 10
// - add a border to the window
// - fix that thing in update
// then the animation looks wrong. Which change caused it?| if it breaks | possible causes |
|---|---|
| after a pure refactoring | the refactoring |
| after a mixed edit | any of four changes |
| how you find out | undo everything and redo it slowly |
Refactoring's value comes from behaviour being unchanged — that is what lets you check it by running the program. Mixing in a change destroys the test.
Refactor, verify, then change.
// step 1: move the code, change nothing else
// run both simulations - identical behaviour?
//
// step 2: NOW change the frame rate| step | expected result |
|---|---|
| extract Automaton | the programs behave exactly as before |
| run both | confirms it |
| then make changes | any difference is from the change |
This is Lesson 4b's incremental development in a new setting. One change at a time, verified between — and refactoring is the one kind of change where the expected result is nothing visible happens.
Definition probe
What belongs in Automaton and what does not.
Sort into buckets
Sort each piece of code.
Fill the middle
Update, redraw, pause.
Fill in the blanks
while (true) update}();
grid.repaint();
try rate});
} catch (InterruptedException e) ___
}
Why: update is the one call whose meaning differs between the two simulations — which is exactly why the next section makes it abstract. Dividing 1000 by the rate turns frames-per-second into a millisecond delay.
Explain it to yourself
Both programs work.
Discussion prompt
Conway and Langton both run correctly with duplicated mains. What is the argument for changing working code?
Hint: What happens when you write a third simulation?
Answer:
Because the duplication multiplies future work. A third simulation means a third copy, and any fix to the window setup has to be made in every one.
And copies drift: one gets a bug fix, another does not, and eventually they behave differently for reasons nobody remembers.
Refactoring is maintenance, not repair. Whenever you see repeated code like main, you should think about ways to remove it — and doing it while the code is understood is far cheaper than doing it after a bug forces you to.
Section
Section 16.3
Concept
If we were not planning to implement any other zero-person games, we could leave well enough alone. But there are a few problems with the current design.
| problem | detail |
|---|---|
| grid is private | making it inaccessible in Conway and Langton |
| making it public | then other unrelated classes would have access too |
| Automaton has no constructors | and there would be no reason to create an instance |
| Automaton does not implement update | but subclasses need to provide one |
All three are the same kind of problem: the class does not say what it means. It means grid is for subclasses, do not instantiate me, and you must supply update — and nothing in the code says any of it.
Notation
Java provides language features to solve these problems.
Annotate
protected sits between private and public — subclasses yes, everyone else no. It is the access level Lesson 14a's Hand needed and did not have.abstract on a class forbids new. An Automaton is not a thing you can have; it is a thing other classes can be.abstract on a method declares it without a body and forces every concrete subclass to supply one.Any class that extends Automaton must provide an implementation of update; the declaration here allows the compiler to check. That last clause is the whole value: the design rule stops being a convention.
Worked example
Here's what Automaton looks like as an abstract class.
public abstract class Automaton {
protected GridCanvas grid;
public abstract void update();
private void mainloop(int rate) {
// this method invokes update
}
public void run(String title, int rate) {
// this method invokes mainloop
}
}| declaration | means |
|---|---|
| public abstract class Automaton | cannot be instantiated |
| protected GridCanvas grid | subclasses can use it; other classes cannot |
| public abstract void update(); | no body — subclasses must supply one |
| private void mainloop(int rate) | a full method, not inherited-visible outside |
| public void run(String, int) | a full method, called by each subclass's main |
Note the missing body.
Why: Notice that the update method has no body. The declaration specifies the name, arguments, and return type. But it does not provide an implementation, because it is an abstract method.
Note the semicolon.
Why: An abstract method ends with ; where a normal one would open a brace.
Note the class keyword.
Why: Notice also the word abstract on the first line, which declares that Automaton is an abstract class.
And the rule linking them.
Why: In order to have any abstract methods, a class must be declared as abstract.
Verify: Try new Automaton() and read the compiler error.
Why: An abstract class is a promise about its subclasses, not a thing in its own right. mainloop calls this.update() — a method that does not exist yet — and that is legal precisely because every concrete subclass is guaranteed to supply one.
Prediction
Automaton is declared abstract.
public abstract class Automaton { ... }
Automaton a = new Automaton();| class is | instantiable? |
|---|---|
| abstract | ? |
Predict first
What happens?
Correct: A compiler error — an abstract class cannot be instantiated
Why: We can make the class abstract, which means it cannot be instantiated. If you attempt to create an object for an abstract class, you will get a compiler error. That is the point: there would be no reason to create an instance of this class, and now the compiler enforces it.
Concept
Here's what Conway looks like as a subclass of Automaton. Everything shared has gone.
public class Conway extends Automaton {
// same methods as before, except mainloop is removed
public static void main(String[] args) {
String title = "Conway's Game of Life";
Conway game = new Conway();
game.run(title, 2);
}
}| before | after | |
|---|---|---|
| mainloop | in Conway | inherited |
| JFrame setup | in Conway's main | inherited via run |
| grid | private in Conway | protected in Automaton |
| update | in Conway | still in Conway — it must be |
| main | 12 lines | 3 lines |
Conway extends Automaton, so it inherits the protected instance variable grid and the methods mainloop and run. But because Automaton is abstract, Conway has to provide update and a constructor (which it has already). Twelve lines of main become three.
Trap
A concrete subclass that does not override update.
public class Brian extends Automaton {
public Brian() { grid = new GridCanvas(50, 50, 10); }
// no update method
}
// error: Brian is not abstract and does not override
// abstract method update() in Automaton| without abstract | with abstract | |
|---|---|---|
| when you find out | at run time, if ever | at compile time |
| symptom | the simulation does nothing | the class will not compile |
If the subclass does not override an abstract method, you will get a compiler error. Without abstract, a forgotten update would inherit an empty one and the simulation would simply sit still — a bug with no message.
Implement it — or declare the subclass abstract too.
public class Brian extends Automaton {
public Brian() { grid = new GridCanvas(50, 50, 10); }
public void update() {
// Brian's Brain rules go here
}
}| class | abstract? | must implement update? |
|---|---|---|
| Automaton | yes | no — it declares it |
| Conway | no — concrete | yes |
| Langton | no — concrete | yes |
A subclass may also be abstract, in which case it passes the obligation down to its subclasses. The rule is only that a class you can actually instantiate must have a body for every method.
Definition probe
Three levels, three audiences.
Sort into buckets
Sort each requirement.
Matching
Chapter 16's vocabulary.
Match the pairs
Why: The three vocabulary entries plus the access level that made the refactoring work. Note that an abstract class may or may not have abstract methods — but a class with any abstract method must itself be abstract.
Counterexample
A do-nothing method body would compile.
Discussion prompt
Instead of public abstract void update();, Automaton could define public void update() { }. What would that cost?
Hint: What happens if a subclass forgets?
Answer:
A subclass that forgot to override it would inherit the empty version and compile perfectly — then sit on screen doing nothing, with no error to explain why.
The abstract declaration turns that into a compile error naming the exact missing method. The bug is caught before the program runs, by the person who can fix it immediately.
A requirement the compiler checks beats one you have to remember — the same argument as final for immutability in Lesson 12a and braces in Lesson 5a. Three chapters apart, one principle.
Section
Section 16.4
Concept
At the beginning of the chapter, we had three classes: Cell, GridCanvas, and Conway. We then developed Langton, which had almost the same main and mainloop methods as Conway. So we refactored the code and created Automaton.
| relationship | kind | example |
|---|---|---|
| Conway is an Automaton | inheritance | extends |
| Langton is an Automaton | inheritance | extends |
| GridCanvas is a Canvas | inheritance | extends |
| Automaton has a GridCanvas | composition | an attribute |
| GridCanvas has a 2D array of Cells | composition | an attribute |
The diagram shows three examples of inheritance and two examples of composition. It also shows a third kind of arrow: Automaton uses JFrame, GridCanvas uses Graphics, and Cell uses Graphics and Color — a class that appears only inside a method, not as an attribute.
Picture it
Figure 16.1 summarizes the final design.
Figure (svg): A UML diagram of Automaton with Conway and Langton as subclasses, and GridCanvas containing Cells
Automaton is in italics to indicate that it is an abstract class. As it happens, Graphics is an abstract class, too — the convention you have been using all along was itself abstract.
Worked example
Conway and Langton are concrete classes, because they provide an implementation for all of their methods.
// abstract - declares update, does not implement it
public abstract class Automaton {
public abstract void update();
}
// concrete - implements every method it has
public class Conway extends Automaton {
public void update() { ... }
}| class | abstract? | can you write new? | why |
|---|---|---|---|
| Automaton | yes | no | update has no body |
| Conway | no | yes | every method is implemented |
| Langton | no | yes | same |
| Graphics | yes | no | a library abstract class |
An abstract class declares what subclasses must do.
Why: In particular, they implement the update method that was declared abstract in Automaton.
A concrete class can be instantiated.
Why: A class that is not declared as abstract; each of its methods must have an implementation.
Abstract classes still carry real code.
Why: run and mainloop are ordinary methods with bodies.
Which is the point.
Why: Abstract classes are essentially incomplete class definitions that specify methods to be implemented by subclasses. But they also provide attributes and methods to be inherited, thus eliminating repeated code.
Verify: Count what Conway gets from Automaton: one attribute and two complete methods, for one method it must supply.
Why: That trade is what makes an abstract class worth more than an empty superclass. It is not just a contract — it is a contract that comes with most of the implementation attached.
Definition probe
Three kinds of arrow in Figure 16.1.
Sort into buckets
Sort each pair.
Concept
One of the challenges of object-oriented programming is keeping track of a large number of classes and the relationships between them. UML class diagrams can help.
| notation | means |
|---|---|
| hollow triangle arrowhead | inheritance — IS-A |
| standard arrowhead | composition — HAS-A |
| a dashed or plain 'uses' arrow | the class appears inside a method |
| italic class name | abstract |
| a minus sign | private |
| a hash sign | protected |
Six classes is already more than fits comfortably in your head, and this is a small program. The diagram is the only representation that shows all the relationships at once — which is why Lesson 14b called it a shared notation worth learning.
Trap
Public solves it, and gives away more than intended.
public class Automaton {
public GridCanvas grid; // now every class can reach it
}
// somewhere unrelated:
someOtherClass.game.grid = null;| access level | subclasses | unrelated classes |
|---|---|---|
| private | no | no |
| public | yes | yes |
| protected | yes | no |
We could make it public, but then other (unrelated) classes would have access to it as well. The problem was never that access was too tight in general — it was too tight for one specific audience.
protected names exactly that audience.
public abstract class Automaton {
protected GridCanvas grid;
}| who | can reach grid? |
|---|---|
| Automaton itself | yes |
| Conway and Langton | yes |
| any future Automaton subclass | yes |
| everything else | no |
Accessible to subclasses but not other classes. Lesson 14a's Hand hit exactly this wall and had to go through public getters instead — protected is the keyword that was missing there, arriving two chapters later with a concrete reason to want it.
Prediction
Automaton declares update abstract.
public abstract class Automaton {
protected GridCanvas grid;
public abstract void update();
public void run(String title, int rate) { ... }
}| member | inherited? |
|---|---|
| grid | yes — protected |
| run | yes |
| update | ? |
Predict first
What must Conway supply?
Correct: An update method and a constructor — everything else is inherited
Why: Because Automaton is abstract, Conway has to provide update and a constructor (which it has already). Constructors are never inherited — Lesson 14a's rule — and update is required because it was declared abstract. grid, run and mainloop all come free.
Fill the middle
A shared attribute and a required method.
Fill in the blanks
public abstract class Automaton protected} GridCanvas grid;
public abstract void update();
}
Why: abstract on the class means it cannot be instantiated — and it is required, since the class has an abstract method. protected gives subclasses access to grid without exposing it to every other class, which is the middle ground private and public do not offer.
Real world
As it happens, Graphics is an abstract class, too.
Discussion prompt
You have been passing Graphics g around for two chapters without being able to create one. Now that you know what abstract means, what does that tell you about Graphics?
Hint: Who supplies the object you receive?
Answer:
You could never have written new Graphics() — it is abstract. The object you receive is a concrete subclass supplied by the window system, specific to whatever is actually being drawn on.
That is why paint receives one rather than making one: only the framework knows which concrete implementation is appropriate — a screen, a printer, an off-screen image.
**An abstract class is how a library says there are several kinds of this, and you do not need to know which one you have.** You program against Graphics; the system decides the rest — which is the same shape as run calling an update it has never seen.
Comparison
Fill the blanks.
Comparison matrix
| private | protected | public | |
|---|---|---|---|
| this class | yes | yes | yes |
| subclasses | no | yes | yes |
| unrelated classes | no | no | yes |
| used in this chapter for | Langton's xpos and head | Automaton's grid | run and update |
protected is the row that changes. It is the only level that distinguishes my subclasses from everyone else — which is precisely the distinction a shared superclass attribute needs.
Pattern
Extract a superclass, then make it abstract so the compiler enforces what it means.
// 1. two classes with duplicated code
public class Conway { /* main, mainloop, update */ }
public class Langton { /* main, mainloop, update */ }
// 2. move what is shared into a superclass;
// what differed becomes a parameter
public class Automaton {
private GridCanvas grid;
public void run(String title, int rate) { ... }
}
// 3. say what you mean, and let the compiler check it
public abstract class Automaton {
protected GridCanvas grid; // subclasses only
public abstract void update(); // subclasses MUST supply
}| intent | keyword | what the compiler now checks |
|---|---|---|
| subclasses may use this | protected | unrelated classes cannot |
| do not instantiate this class | abstract class | new Automaton() is rejected |
| every subclass must supply this | abstract method | a missing update is a compile error |
| restructure without changing behaviour | — | you check by running it |
(x + k − 1) % k steps backward around a cycle without going negative.Check
Work it out before you click.
head = (head + 3) % 4; // turn left
// why not (head - 1) % 4 ?| expression | when head is 0 |
|---|---|
| (0 - 1) % 4 | -1 |
| (0 + 3) % 4 | 3 |
Check your understanding
Why does the code add 3 instead of subtracting 1?
Answer: A
Why: To turn left, we could subtract 1, but -1 % 4 is -1 in Java. So we add 3 instead, since one left turn is the same as three right turns. Java's remainder takes the sign of the dividend, so subtracting can leave head negative — and moveAnt's final else would silently absorb it.
Check
Work it out before you click.
public abstract class Automaton {
public abstract void update();
}
public class Brian extends Automaton {
public Brian() { ... }
// no update method
}| class | abstract? |
|---|---|
| Automaton | yes |
| Brian | no |
Check your understanding
What happens?
Answer: A
Why: If the subclass does not override an abstract method, you will get a compiler error. That is the whole reason for declaring it abstract rather than giving it an empty body — a forgotten override becomes a message naming the missing method rather than a simulation that silently sits still.
abstract for you; you must write it if you intend the subclass to stay incomplete.Check
Work it out before you click.
public abstract class Automaton {
protected GridCanvas grid;
}| who | access? |
|---|---|
| Conway extends Automaton | ? |
| an unrelated class | ? |
Check your understanding
Who can access grid?
Answer: A
Why: We can make the grid attribute protected, which means it's accessible to subclasses but not other classes. private was too strict — it excluded Conway and Langton, exactly as it excluded Hand in Lesson 14a — and public would have been too loose, exposing the grid to everything.
Real world
This process of reorganizing existing code, without changing its behavior, is known as refactoring.
Discussion prompt
Why does it help to have a name for it — and what does having a name make possible that tidying up does not?
Hint: What can you say to a colleague, and what can a tool do?
Answer:
A name makes it a describable activity. I'm refactoring tells a colleague that behaviour will not change and the tests should still pass — which is a much more precise promise than tidying up.
And it makes tooling possible. Every serious IDE has refactoring commands — extract method, rename, pull members up to a superclass — that perform exactly this chapter's transformations mechanically.
Most usefully, it separates two kinds of work. Improving structure and changing behaviour are different jobs with different risks, and the discipline is to never do both at once — which is only easy to say once the first one has a name.
Commit first
Commit to an answer and to your confidence.
Predict first
What does declaring a method abstract accomplish?
Correct: It declares the signature without a body and forces every concrete subclass to implement it
Why: We can declare update as an abstract method, meaning that it must be overridden in subclasses. If the subclass does not override an abstract method, you will get a compiler error. The declaration gives the name, arguments and return type but no implementation — which is exactly what lets mainloop call this.update() inside a class that has no idea what update does. Note the dependency in the other direction: in order to have any abstract methods, a class must be declared as abstract, so you cannot put one in an ordinary class.
Explain it
Two minutes, out loud.
Discussion prompt
A classmate has two simulation classes with identical main methods and asks whether it is worth doing anything about it. Walk them through the refactoring and say what abstract adds on top.
Hint: Extract, parameterise, then enforce.
Answer:
Move what is identical into a superclass method, and turn what differed into parameters — the title and the frame rate here. Both classes then extend it, and each main becomes three lines.
Then make the superclass abstract, because nobody should ever create one, and declare the method that differs — update — abstract too, so a subclass that forgets it fails to compile.
And do the refactoring on its own first: run both programs before and after and check nothing changed. Refactoring means restructuring without changing behaviour, and that is what makes it checkable.
Exit ticket
One question before you close the deck.
Predict first
What is the difference between an abstract class and a concrete class?
Correct: An abstract class cannot be instantiated and may declare methods without bodies; a concrete class implements all of its methods
Why: Abstract class: a class that is declared as abstract; it cannot be instantiated, and it may (or may not) include abstract methods. Concrete class: a class that is not declared as abstract; each of its methods must have an implementation. The last option is the tempting mistake — Automaton contains run and mainloop in full, and that is exactly what makes it worth extending: abstract classes are essentially incomplete class definitions that specify methods to be implemented by subclasses, but they also provide attributes and methods to be inherited, thus eliminating repeated code.
Connect it up
One page, from memory.
Draw it
Draw the six-class UML diagram — Automaton in italics, Conway, Langton, Canvas, GridCanvas, Cell — using hollow triangle heads for inheritance and plain heads for composition, and mark grid with a hash sign for protected. Beside it, write the four headings 0 to 3 in a circle and show why turning right is +1 and turning left is +3. Then write the three problems with the non-abstract Automaton and the keyword that fixes each. Finish with the one-sentence definition of refactoring, underlining the six words that matter.
Recap
One chapter, one argument: write a second simulation, notice the duplication, remove it, then say what you mean.
| if you remember one thing | it is this |
|---|---|
| about duplication | the second copy is the signal, not the tenth |
| about refactoring | structure now, behaviour later — never both |
| about abstract | turn a design rule into a compiler check |
head encodes four directions clockwise, so turning right is (head + 1) % 4.-1 % 4 is -1 in Java.protected means subclasses but not other classes — the level neither private nor public provides.Want this taught 1-on-1? Alexander tutors Java — $55/session, free consultation.