Refactoring, Abstract Classes, and UML

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

What this lesson covers

The lesson, slide by slide

1. Refactoring, Abstract Classes, and UML

Title

Think Java 2e · Chapter 16 · Reusing Classes

Sections 16.1-16.4 · pp. 267-274

2. What you will be able to do

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.

3. Retrieve before you read

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.

4. A second game, and what it exposes

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.

5. Langton's Ant

Section

Section 16.1

6. Two rules, and unpredictable behaviour

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.
phaseroughly how many stepswhat it looks like
earlythe first few hundredsmall symmetric patterns
chaoticup to about 10,000seemingly random
the highwaya repeating loop of 104 stepsa 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.

7. Where the ant starts

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.

8. The Langton class

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;
    }
}
fieldholdschanges?
grida GridCanvas — the state of the cellsits cells do
xposthe ant's columnevery step
yposthe ant's rowevery step
headwhich way it faces: 0 to 3every 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.

9. What does head = 0 mean?

Prediction

Four values, one per direction.

private int head;   // 0=North, 1=East, 2=South, 3=West
headdirection
0?
1east

Predict first

Which way is the ant facing?

  • North — toward the top of the screen
  • East
  • South
  • It has not been set yet

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.

10. Encoding a direction

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.

headdirectionmoving forward means
0northypos − 1
1eastxpos + 1
2southypos + 1
3westxpos − 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.

11. Confusing xpos and ypos with rows and columns

Trap

The 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 expectsthe call passes
first argumenta row indexxpos
second argumenta column indexypos
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.

The fix

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;
habitwhy
name grid indexes r and cso a swap looks wrong
name pixel coordinates x and yso the two never blur
test on a non-square gridit 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.

12. Which class is responsible?

Definition probe

Cell and GridCanvas were not modified.

Sort into buckets

Sort each responsibility.

Cell
storing whether one square is black
GridCanvas
the 2D array of squares
Langton
where the ant is and which way it faces; the two rules of Langton's Ant
cell
A cell owns its own state and appearance, and knows nothing about which simulation it is in.
grid
GridCanvas owns the array and the drawing — general machinery that serves any grid simulation unchanged.
lang
Everything specific to this game: the ant's position, its heading, and the rules that move it.

13. The Langton constructor

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.

14. Why does simple produce complicated?

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.

15. Turning with the remainder operator

Section

Section 16.1

16. flipCell does all three parts of a rule

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();
    }
}
cellturnnew head from 0cell becomes
off — whiteright(0 + 1) % 4 = 1on
on — blackleft(0 + 3) % 4 = 3off

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.

17. Why turning left adds three

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.
  • A head of −1 would index nothing sensible and moveAnt's chain of ifs would fall through to the else — the ant would move west when it should move west... by accident.
  • One left turn is the same as three right turns, so adding 3 gets the same answer while keeping the value non-negative.
  • The clockwise encoding is what makes this work. Had the four directions been in a different order, neither +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.

18. Moving forward

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;
    }
}
headdirectionchange
0northypos − 1 — up the screen
1eastxpos + 1
2southypos + 1 — down the screen
3westxpos − 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.

19. Turning right from west

Prediction

head is 3; the ant is on a white cell.

head = (head + 1) % 4;
headhead + 1% 4
34?

Predict first

What is the new head?

  • 0 — north
  • 4
  • 3
  • -1

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.

20. update, and what it does not do

Concept

Langton's update is two lines, and notably shorter than Conway's.

public void update() {
    flipCell();
    moveAnt();
}
Conway.updateLangton.update
cells changed per steppotentially all of themexactly one
needs a snapshot?yes — a counts arrayno
reads neighbours?yes, eight of themno
linesabout 30 with its helpers2, 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.

21. Subtracting one to turn left

Trap

The trap

(head - 1) % 4 goes negative.

head = (head - 1) % 4;    // turn left - WRONG

// with head == 0:
//   (0 - 1) % 4  ->  -1 % 4  ->  -1 in Java
headexpressionresultvalid?
3(3 − 1) % 42yes
1(1 − 1) % 40yes
0(0 − 1) % 4−1no

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.

The fix

Add three instead, and stay non-negative.

head = (head + 3) % 4;    // turn left
head(head + 3) % 4means
0 — north3west — correct left turn
1 — east0north
2 — south1east
3 — west2south

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.

22. What is -1 % 4 in Java?

Prediction

The sign follows the left operand.

System.out.println(-1 % 4);
expressionvalue
7 % 43
-1 % 4?

Predict first

What is printed?

  • -1
  • 3
  • 1
  • 0

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.

23. Turn in both directions

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.

24. What if head went out of range?

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.

25. Refactoring

Section

Section 16.2

26. Two copies of the same main

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();
}
linesame as Conway's?
the title stringno
which game object is createdno
the JFrame construction and configurationyes, identical
add, pack, setVisibleyes
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.

27. Refactoring, defined

Notation

The chapter names the technique it has just used twice.

Annotate

  • Refactoring changes structure, not behaviour. The program does exactly what it did before — that is what distinguishes it from adding a feature or fixing a bug.
  • Which means you can test it. Run both simulations before and after; if anything looks different, the refactoring was wrong.
  • Repeated code is the trigger, exactly as it was in Chapter 14 when Deck and Pile turned out to be two versions of the same class.
  • Doing one thing at a time is the point. Refactoring and adding features at once means a failure could be either.
  • It is worth doing even when nothing is broken — the duplication has not caused a bug yet, and removing it is still the right move.

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.

28. Extracting Automaton

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 Automatonwas in
the grid fieldboth Conway and Langton
JFrame construction and configurationboth mains
add, pack, setVisibleboth mains
the call to mainloopboth 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.

29. What is the delay when rate is 4?

Prediction

The rate is frames per second.

Thread.sleep(1000 / rate);
rate1000 / rate
2500
4?

Predict first

How long does each pause last?

  • 250 milliseconds
  • 4000 milliseconds
  • 4 milliseconds
  • 1000 milliseconds

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.

30. mainloop, with a frame rate

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
        }
    }
}
rate1000 / rateframes per second
11000 ms1
2500 ms2
10100 ms10
10001 ms1000 — 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.

31. Refactoring and changing behaviour at once

Trap

The 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 breakspossible causes
after a pure refactoringthe refactoring
after a mixed editany of four changes
how you find outundo 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.

The fix

Refactor, verify, then change.

// step 1: move the code, change nothing else
//         run both simulations - identical behaviour?
//
// step 2: NOW change the frame rate
stepexpected result
extract Automatonthe programs behave exactly as before
run bothconfirms it
then make changesany 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.

32. Shared or specific?

Definition probe

What belongs in Automaton and what does not.

Sort into buckets

Sort each piece of code.

Automaton
creating and configuring the JFrame; the while loop with update, repaint and sleep
the subclass
the two rules of Langton's Ant; counting live neighbours
auto
Both simulations do it identically, so it belongs in the superclass where it is written once.
sub
It is specific to one simulation — the rules are exactly what makes Conway and Langton different.

33. The shared loop

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.

34. Why refactor when nothing is broken?

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.

35. Abstract classes

Section

Section 16.3

36. Three problems with the design so far

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.

problemdetail
grid is privatemaking it inaccessible in Conway and Langton
making it publicthen other unrelated classes would have access too
Automaton has no constructorsand there would be no reason to create an instance
Automaton does not implement updatebut 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.

37. Three keywords, three fixes

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.
  • Each fix turns a comment into a compiler check. The intent was already there; the keywords make it enforceable.
  • In order to have any abstract methods, a class must be declared as abstract — the one dependency between the three.

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.

38. Automaton as an abstract class

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
    }
}
declarationmeans
public abstract class Automatoncannot be instantiated
protected GridCanvas gridsubclasses 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.

39. What does new Automaton() do?

Prediction

Automaton is declared abstract.

public abstract class Automaton { ... }

Automaton a = new Automaton();
class isinstantiable?
abstract?

Predict first

What happens?

  • A compiler error — an abstract class cannot be instantiated
  • It creates an object with no update method
  • It works, but update does nothing
  • It throws an exception at run time

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.

40. Conway as a subclass

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);
    }
}
beforeafter
mainloopin Conwayinherited
JFrame setupin Conway's maininherited via run
gridprivate in Conwayprotected in Automaton
updatein Conwaystill in Conway — it must be
main12 lines3 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.

41. Forgetting to implement an abstract method

Trap

The 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 abstractwith abstract
when you find outat run time, if everat compile time
symptomthe simulation does nothingthe 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.

The fix

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
    }
}
classabstract?must implement update?
Automatonyesno — it declares it
Conwayno — concreteyes
Langtonno — concreteyes

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.

42. Which access level?

Definition probe

Three levels, three audiences.

Sort into buckets

Sort each requirement.

private
usable only inside this class
protected
usable by subclasses but no other class; Automaton's grid attribute
public
usable by any class
priv
The strictest level — even subclasses are excluded, which is exactly the problem Automaton had.
prot
Between the two: subclasses can reach it, unrelated classes cannot. Precisely what a shared attribute in a superclass needs.
pub
No restriction at all, which for an attribute gives away more than intended.

43. Match the term to its definition

Matching

Chapter 16's vocabulary.

Match the pairs

  • a. refactor
  • b. abstract class
  • c. concrete class
  • d. protected
  • r1. restructure existing code without changing its behavior
  • r2. cannot be instantiated; may include abstract methods
  • r3. not declared abstract; every method has an implementation
  • r4. accessible to subclasses but not to other classes

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.

44. Could Automaton just have an empty update?

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.

45. The finished design

Section

Section 16.4

46. Five classes and four relationships

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.

relationshipkindexample
Conway is an Automatoninheritanceextends
Langton is an Automatoninheritanceextends
GridCanvas is a Canvasinheritanceextends
Automaton has a GridCanvascompositionan attribute
GridCanvas has a 2D array of Cellscompositionan 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.

47. The whole design

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.

48. Abstract against concrete

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() { ... }
}
classabstract?can you write new?why
Automatonyesnoupdate has no body
Conwaynoyesevery method is implemented
Langtonnoyessame
Graphicsyesnoa 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.

49. Which relationship?

Definition probe

Three kinds of arrow in Figure 16.1.

Sort into buckets

Sort each pair.

IS-A — inheritance
Conway and Automaton; GridCanvas and Canvas
HAS-A — composition
Automaton and GridCanvas
uses
Automaton and JFrame
isa
One class extends the other, so every instance of the subclass is also an instance of the superclass.
hasa
One class holds an instance of the other as an attribute.
uses
The class appears only inside a method — a JFrame is created in run and never stored — so it is neither inherited nor held.

50. Why UML earns its keep here

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.

notationmeans
hollow triangle arrowheadinheritance — IS-A
standard arrowheadcomposition — HAS-A
a dashed or plain 'uses' arrowthe class appears inside a method
italic class nameabstract
a minus signprivate
a hash signprotected

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.

51. Making grid public to fix the access problem

Trap

The 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 levelsubclassesunrelated classes
privatenono
publicyesyes
protectedyesno

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.

The fix

protected names exactly that audience.

public abstract class Automaton {
    protected GridCanvas grid;
}
whocan reach grid?
Automaton itselfyes
Conway and Langtonyes
any future Automaton subclassyes
everything elseno

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.

52. What must a subclass of Automaton provide?

Prediction

Automaton declares update abstract.

public abstract class Automaton {
    protected GridCanvas grid;
    public abstract void update();
    public void run(String title, int rate) { ... }
}
memberinherited?
gridyes — protected
runyes
update?

Predict first

What must Conway supply?

  • An update method and a constructor — everything else is inherited
  • update, run and mainloop
  • Nothing; it all comes from Automaton
  • A grid attribute of its own

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.

53. Declare the abstract class

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.

54. Where else do abstract classes appear?

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.

55. The three access levels, and the two class kinds

Comparison

Fill the blanks.

Comparison matrix

privateprotectedpublic
this classyesyesyes
subclassesnoyesyes
unrelated classesnonoyes
used in this chapter forLangton's xpos and headAutomaton's gridrun 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.

56. The pattern to carry away

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
}
intentkeywordwhat the compiler now checks
subclasses may use thisprotectedunrelated classes cannot
do not instantiate this classabstract classnew Automaton() is rejected
every subclass must supply thisabstract methoda missing update is a compile error
restructure without changing behaviour—you check by running it

57. Check: turning left

Check

Work it out before you click.

head = (head + 3) % 4;    // turn left
// why not (head - 1) % 4 ?
expressionwhen head is 0
(0 - 1) % 4-1
(0 + 3) % 43

Check your understanding

Why does the code add 3 instead of subtracting 1?

  • A. Because -1 % 4 is -1 in Java, and one left turn equals three right turns (correct)
  • B. Because subtraction is slower than addition
  • C. Because head is unsigned
  • D. Because there are three directions to skip

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.

Why B tempts people
The speed difference is nil; correctness is the issue.
Why C tempts people
Java has no unsigned int type; that is exactly why the sign matters.
Why D tempts people
Three is used because it is equivalent to −1 modulo 4, not because anything is skipped.

58. Check: abstract

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
}
classabstract?
Automatonyes
Brianno

Check your understanding

What happens?

  • A. A compile error — a concrete subclass must implement every abstract method it inherits (correct)
  • B. It compiles and update does nothing
  • C. It compiles and throws an exception when update is called
  • D. Brian automatically becomes abstract

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.

Why B tempts people
That would be the behaviour if Automaton defined an empty update, which is precisely the design this replaces.
Why C tempts people
Java catches it at compile time, not run time.
Why D tempts people
Java never adds abstract for you; you must write it if you intend the subclass to stay incomplete.

59. Check: protected

Check

Work it out before you click.

public abstract class Automaton {
    protected GridCanvas grid;
}
whoaccess?
Conway extends Automaton?
an unrelated class?

Check your understanding

Who can access grid?

  • A. Automaton and its subclasses, but no other classes (correct)
  • B. Only Automaton
  • C. Every class
  • D. Only classes in the same package

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.

Why B tempts people
That is what private means, and it is the problem protected solves.
Why C tempts people
That is public; the chapter rejects it because unrelated classes would gain access too.
Why D tempts people
Package access is Java's default with no keyword; protected also reaches subclasses outside the package.

60. Refactoring is a named practice

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.

61. How sure are you?

Commit first

Commit to an answer and to your confidence.

Predict first

What does declaring a method abstract accomplish?

  • It declares the signature without a body and forces every concrete subclass to implement it
  • It makes the method faster
  • It prevents subclasses from overriding it
  • It makes the method static

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.

62. Explain it to someone else

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.

63. Exit ticket

Exit ticket

One question before you close the deck.

Predict first

What is the difference between an abstract class and a concrete class?

  • An abstract class cannot be instantiated and may declare methods without bodies; a concrete class implements all of its methods
  • An abstract class has no attributes
  • A concrete class cannot be extended
  • An abstract class cannot contain any real code

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.

64. Draw the whole lesson

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.

65. Recap

Recap

One chapter, one argument: write a second simulation, notice the duplication, remove it, then say what you mean.

if you remember one thingit is this
about duplicationthe second copy is the signal, not the tenth
about refactoringstructure now, behaviour later — never both
about abstractturn a design rule into a compiler check

Sources

  1. 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
  2. The Java Tutorials — Abstract Methods and Classes
  3. Think Java 2e — free online edition and source code

Want this taught 1-on-1? Alexander tutors Java — $55/session, free consultation.

Book on Wyzant · Text (657) 465-8108