A while loop that never ends, a compiler error that forces you to learn try-catch, and then try-catch used again on purpose — to let a cell on the edge of the grid count neighbours that do not exist. Finished by the two-pass update that makes all the cells change at once. Follows Think Java 2e, Chapter 15 (Arrays of Arrays), Sections 15.7-15.10, pp. 257-263, cross-referenced against The Java Tutorials — Catching and Handling Exceptions.
Subject: Java · 65 slides · code lesson
Open the interactive version of this deck
Title
Think Java 2e · Chapter 15 · Arrays of Arrays
Sections 15.7-15.10 · pp. 257-263
Objectives
This lesson follows Think Java 2e, Chapter 15 (Arrays of Arrays), Sections 15.7-15.10, pp. 257-263. Everything on these slides can be checked against those pages.
1. Write a simulation loop that updates, repaints and pauses.
2. Explain the difference between calling repaint and calling paint.
3. Write a try-catch statement and say exactly which code runs in each case.
4. Use an exception to handle out-of-bounds array access as an ordinary case.
5. Count the eight neighbours of a grid cell without writing special cases for the edges.
6. Implement a two-pass update so that every cell changes simultaneously.
Warm-up
Three things this lesson needs.
Discussion prompt
From Lesson 7a: what happens when you index past the end of an array? From Lesson 15a: why must all cells update simultaneously? And what is the difference between paint and repaint?
Hint: An exception, a corrupted count, and who calls whom.
Answer:
Indexing out of bounds throws an ArrayIndexOutOfBoundsException and, so far, ends the program. Cells must update simultaneously because each one's next state depends on its neighbours' current states. And you write paint; the window system calls it — you call repaint.
This lesson turns the first of those from a crash into a technique. Now that you know about try and catch, we can use them to implement a useful method in GridCanvas — the one that lets a corner cell ask about neighbours it does not have.
Concept
Most cells have eight neighbors. However, cells on the edges and in the corners have fewer neighbors. If we try to count all possible neighbors, we'll go out of bounds.
Figure (svg): A grid showing an interior cell with eight neighbours and a corner cell with only three
Downey & Mayfield, Think Java, 2nd edition (Green Tea Press / O'Reilly, 2020) — Think Java 2e, Chapter 15 (Arrays of Arrays), Sections 15.7-15.10, pp. 257-263 — Section 15.7 opens on printed page 257.
Section
Section 15.7
Concept
At the end of main, we call mainloop, which uses a while loop to simulate the time steps of the Game of Life. Here's a rough draft of this method.
private void mainloop() {
while (true) {
this.update();
grid.repaint();
Thread.sleep(500); // compiler error
}
}| line | does |
|---|---|
| while (true) | run forever — the window's close button ends the program |
| this.update() | compute the next generation |
| grid.repaint() | ask the system to redraw |
| Thread.sleep(500) | wait half a second |
During each time step, we update the state of the game and repaint the grid. We will present the update method in Section 15.10. Three lines, and the last one does not compile — which is how the chapter introduces exceptions.
Notation
Every one of the three does something the animation would fail without.
Annotate
paint when it is ready, which is why no Graphics object is needed.while (true) has no exit, which is deliberate here: a zero-player game has no end condition, and the window's close button stops the program.Model, view, timing — three concerns, three lines. Almost every animation loop you ever write will have these same three parts.
Worked example
There's just one problem: compiling this code results in the error unreported exception InterruptedException.
Thread.sleep(500);
// error: unreported exception InterruptedException;
// must be caught or declared to be thrown| the exceptions so far | InterruptedException | |
|---|---|---|
| when you find out | at run time | at compile time |
| examples | array index out of bounds, null pointer | sleep being interrupted |
| can you ignore it? | yes, until it happens | no — it will not compile |
Notice this error is different.
Why: It is not the program crashing — the program never ran.
The compiler is enforcing something.
Why: Thread.sleep announces that it may be interrupted, and Java insists you say what to do about it.
This message means we need to do some exception handling.
Why: Which is the next section.
Two ways to satisfy it.
Why: Catch it here, or declare that your method throws it too. The book catches it.
Verify: Compile the draft loop and read the error text before fixing it.
Why: So far, the only exceptions you have seen are run-time errors like array index out of bounds and null pointer. Those you discover by running. This one the compiler refuses to let you ignore — which is a distinction worth noticing even though the chapter does not dwell on it.
Prediction
The units matter.
Thread.sleep(500);| argument | unit |
|---|---|
| 500 | milliseconds |
Predict first
How long does the program pause?
Correct: Half a second
Why: Thread.sleep(500) causes the program to sleep for 500 milliseconds, or a half second. Milliseconds are the usual unit for timing in Java, so a 500 here gives two generations per second — fast enough to look like animation and slow enough to follow.
Concept
mainloop is private and belongs to Conway, not to GridCanvas — and the split follows Lesson 15a's division of responsibility.
| class | responsible for |
|---|---|
| Cell | its own state and how to draw itself |
| GridCanvas | the 2D array, and drawing all the cells |
| Conway | the rules, the update, and the timing |
GridCanvas knows nothing about the Game of Life — it would serve any grid-based simulation unchanged. That is the same separation as Lesson 14b's card classes and Player: the general machinery in one class, the rules in another.
Trap
Without sleep, the animation is unwatchable.
while (true) {
update();
grid.repaint();
// no pause
}| consequence | detail |
|---|---|
| thousands of generations per second | you see a blur, or nothing |
| repaint requests pile up | the system cannot keep pace |
| the processor runs flat out | for no benefit |
Otherwise the program would run so fast we would not be able to see the animation. The simulation is still correct — it is the display that becomes useless, which is a reminder that works and usable are different tests.
Pause between time steps, and choose the interval.
while (true) {
update();
grid.repaint();
try {
Thread.sleep(500);
} catch (InterruptedException e) {
// do nothing
}
}| sleep | generations per second |
|---|---|
| 500 | 2 |
| 100 | 10 |
| 1000 | 1 |
A sleep in a loop is how nearly every animation controls its speed. The number is a tuning knob: small enough to look alive, large enough to follow.
Prediction
Both eventually draw the grid.
grid.repaint(); // not grid.paint(???)| method | parameter |
|---|---|
| paint(Graphics g) | needs a Graphics object |
| repaint() | none |
Predict first
What is the reason?
Correct: repaint needs no Graphics object — the system supplies one when it calls paint
Why: The reason we use it here is that repaint does not require a Graphics object as a parameter. Only the window system can produce a valid graphics context for the current screen, so your code asks for a redraw and the system decides when and with what to do it.
Definition probe
Model, view and rules.
Sort into buckets
Sort each responsibility.
Explain it to yourself
Lesson 6a called an unconditional loop a hazard.
Discussion prompt
mainloop never exits. Why is that the right design for this program, when it would be a bug in most others?
Hint: What ends a zero-player game?
Answer:
A zero-player game has no end condition. There is no winner and no final state — a pattern can stabilise, but the simulation is still running.
The program ends when the user closes the window, which setDefaultCloseOperation(EXIT_ON_CLOSE) arranged in Lesson 15a's main.
A loop with no condition is fine when the exit genuinely lives outside the loop — a window close, a signal, a return. It is a bug when there was supposed to be a condition and you forgot it.
Section
Section 15.8
Concept
When one of these exceptions occurs, Java displays a message and ends the program. If you don't want the program to end, you can handle exceptions with a try-catch statement. The syntax is similar to an if-else statement, and the logic is, too.
try {
Thread.sleep(500);
} catch (InterruptedException e) {
// do nothing
}| what happens in the try block | then |
|---|---|
| no exception | the catch block does not run |
| an InterruptedException | the catch block runs |
| a different exception | Java does what it would otherwise — probably end the program |
First, Java runs the code in the try block, which calls Thread.sleep in this example. If an InterruptedException occurs during the try block, Java executes the catch block. The catch names the type it handles — anything else passes straight through.
Picture it
Like an if-else, but the branch is chosen by what went wrong rather than by a condition you wrote.
Figure (svg): A flow diagram showing the three outcomes of a try block: success, a caught exception, and an uncaught one
If no exceptions occur during the try block, the catch block doesn't run and the program continues. And a mismatched exception is not silently swallowed — it behaves exactly as if there were no try at all.
Worked example
In this example, the catch block contains a comment, so it doesn't do anything. That is a decision, not an omission.
try {
Thread.sleep(500);
} catch (InterruptedException e) {
// do nothing
}| option | what the catch block would do |
|---|---|
| ignore it | nothing — the loop continues |
| report it | print a message |
| give up | end the program |
| recover | prompt the user to try again |
Decide what the exception means here.
Why: An interrupted sleep means the pause was cut short. The simulation is unharmed.
So doing nothing is correct.
Why: The effect of the try-catch statement is to ignore an interrupted exception if it occurs.
Note the alternatives exist.
Why: As an alternative, we could use the catch block to display a customized message, end the program, or handle the exception in whatever way is appropriate.
And when they would be right.
Why: For example, if user input causes an exception, we could catch the exception and prompt the user to try again later.
Verify: Ask what would be lost if this particular exception were reported: an occasional message about a shortened pause.
Why: An empty catch is right when the exception genuinely does not matter, and dangerous when it does. The comment // do nothing is what marks the difference between a considered decision and an oversight — which is why it is worth writing rather than leaving the block bare.
Prediction
The try block succeeds.
try {
Thread.sleep(500);
} catch (InterruptedException e) {
System.out.println("interrupted");
}
System.out.println("done");| block | runs? |
|---|---|
| try | yes |
| catch | ? |
Predict first
What is printed?
Correct: Just done — the catch block does not run
Why: If no exceptions occur during the try block, the catch block doesn't run and the program continues. The logic really is like if-else: exactly one of the two blocks runs to completion, and execution carries on afterwards either way.
Concept
The exceptions you have met behave differently from this one, and the difference is about when you find out.
| the ones so far | InterruptedException | |
|---|---|---|
| examples | ArrayIndexOutOfBounds, NullPointer | InterruptedException, FileNotFound |
| discovered | when the program runs | when it compiles |
| must you handle it? | no | yes — or it will not compile |
| typical cause | a bug in your code | something outside your control |
The pattern behind the split: exceptions caused by bugs are not forced on you, because the answer is to fix the bug. Exceptions caused by the outside world — a file that is missing, a sleep interrupted — are forced, because no amount of correct code can prevent them.
Trap
A broad catch hides bugs you needed to see.
try {
update();
grid.repaint();
Thread.sleep(500);
} catch (Exception e) {
// do nothing
}| what could be swallowed | consequence |
|---|---|
| InterruptedException | fine — intended |
| NullPointerException in update | silently skipped, forever |
| ArrayIndexOutOfBounds | the simulation quietly stops working |
| any bug you introduce later | invisible |
The program keeps running and stops doing anything useful. A bug that crashes is easier to fix than one that is caught and ignored — the crash at least tells you where it happened.
Catch the specific exception, around the specific line.
update();
grid.repaint();
try {
Thread.sleep(500);
} catch (InterruptedException e) {
// do nothing
}| choice | effect |
|---|---|
| only sleep is inside the try | bugs in update still crash loudly |
| only InterruptedException is caught | everything else propagates |
| the catch is empty | and that is a considered decision |
Keep the try block as small as the thing that can throw. Two lines moved outside the try is the whole fix, and it is the difference between ignoring one harmless interruption and ignoring every bug in the program.
Prediction
The try block throws a NullPointerException.
try {
somethingThatThrowsNullPointer();
} catch (InterruptedException e) {
// do nothing
}| thrown | caught here? |
|---|---|
| InterruptedException | yes |
| NullPointerException | ? |
Predict first
What happens?
Correct: It is not caught — Java does what it would otherwise, probably ending the program
Why: If a different exception occurs during the try block, Java does whatever it would do otherwise, which is probably to display a message and end the program. The catch names one type, and only that type is diverted — which is exactly why naming a narrow type is safer than catching Exception.
Fill the middle
Wrap the one line that can throw.
Fill in the blanks
try catch ___ (InterruptedException e) ___
Why: The structure parallels if-else: the try block runs first, and the catch block runs only if the named exception occurs. Keeping only Thread.sleep inside the try means bugs in the rest of the loop still crash loudly rather than being swallowed.
Socratic
Here it is exactly right.
Discussion prompt
The book's catch block is a comment. When would an empty catch be a serious mistake, and what should go in it instead?
Hint: What does the exception mean for the rest of the program?
Answer:
When the exception means something is broken and the code after it assumes otherwise. Ignoring a failed file read and then using the data you did not get is worse than crashing.
The right contents depend on what the caller can do: report it, retry, ask the user, or end the program cleanly. Exercise 15.3 does the last of these — printStackTrace and System.exit(1).
The test is whether the program is still correct afterwards. An interrupted sleep leaves the simulation exactly as valid as before, which is what makes ignoring it a genuine decision rather than a shrug.
Section
Section 15.9
Concept
Now that you know about try and catch, we can use them to implement a useful method in GridCanvas. The problem is that cells on the edges and in the corners have fewer neighbors. If we try to count all possible neighbors, we'll go out of bounds.
public int test(int r, int c) {
try {
if (array[r][c].isOn()) {
return 1;
}
} catch (ArrayIndexOutOfBoundsException e) {
// cell doesn't exist
}
return 0;
}| r, c | in bounds? | cell on? | returns |
|---|---|---|---|
| 2, 3 | yes | yes | 1 |
| 2, 4 | yes | no | 0 |
| -1, 3 | no | — | 0 — the exception is caught |
| 99, 3 | no | — | 0 |
The test method takes a row index and a column index. It tries to look up the Cell at that location. If both indexes are in bounds, the Cell exists. In that case, test returns 1 if the Cell is on. Otherwise, it skips the catch block and returns 0.
Notation
Four small decisions, and every one of them matters.
Annotate
return 0 is outside the try, so it is reached both when the cell is off and when it does not exist.return 1 is inside the try and only happens for a real, live cell.Because test handles out-of-bounds exceptions, countAlive works for any values of r and c. That is the whole payoff: the caller never checks anything.
Worked example
Now we can use test to implement countAlive, which takes a grid location and returns the number of live neighbors surrounding that location.
private int countAlive(int r, int c) {
int count = 0;
count += grid.test(r - 1, c - 1);
count += grid.test(r - 1, c);
count += grid.test(r - 1, c + 1);
count += grid.test(r, c - 1);
count += grid.test(r, c + 1);
count += grid.test(r + 1, c - 1);
count += grid.test(r + 1, c);
count += grid.test(r + 1, c + 1);
return count;
}| call | relative position |
|---|---|
| test(r − 1, c − 1) | above left |
| test(r − 1, c) | above |
| test(r − 1, c + 1) | above right |
| test(r, c − 1) | left |
| test(r, c + 1) | right |
| test(r + 1, c − 1) | below left |
| test(r + 1, c) | below |
| test(r + 1, c + 1) | below right |
Eight neighbours, eight calls.
Why: All eight combinations of −1, 0 and +1 — except (0, 0).
The cell itself is deliberately skipped.
Why: There is no test(r, c) — a cell is not its own neighbour.
Each call adds 0 or 1.
Why: count += accumulates, which is Lesson 8b's pattern.
No bounds checking anywhere.
Why: Because test handles out-of-bounds exceptions, countAlive works for any values of r and c.
Verify: Call countAlive(0, 0) on a corner and confirm it returns a number between 0 and 3.
Why: Five of the eight calls go out of bounds for a corner cell, and every one of them quietly returns 0. The alternative — checking r > 0 && c > 0 before each lookup — is eight conditions instead of one try-catch, and every one is a chance to get an inequality backwards.
Prediction
Cell (0, 0) of any grid.
countAlive(0, 0);
// eight calls, five of them out of bounds| offset | position | in bounds? |
|---|---|---|
| (−1, −1), (−1, 0), (−1, 1) | above | no |
| (0, −1) | left | no |
| (0, 1), (1, 0), (1, 1) | right and below | yes |
| (1, −1) | below left | no |
Predict first
How many of the eight lookups can return 1?
Correct: 3
Why: A corner has neighbours only to one side and below, so three lookups are in bounds and five throw. The non-existent cells around the perimeter are considered to be off, so the five contribute zero and the method still returns a sensible count without a single bounds check.
Concept
You could avoid the exception entirely. It is worth seeing what that costs.
// with bounds checking instead:
private int test(int r, int c) {
if (r < 0 || r >= numRows() || c < 0 || c >= numCols()) {
return 0;
}
return array[r][c].isOn() ? 1 : 0;
}| try-catch | explicit checks | |
|---|---|---|
| lines | fewer | more |
| chances to get an inequality wrong | none | four |
| works if the grid is ragged | yes | only with more care |
| speed when out of bounds | slower — exceptions cost | faster |
Both are correct. The explicit version is faster when out-of-bounds lookups are common, and the try-catch version is shorter and cannot get an inequality backwards. Think Java chooses the second — and the important thing is knowing that it is a choice.
Trap
A ninth call, for (r, c).
count += grid.test(r, c); // the cell ITSELF - not a neighbour| cell | true neighbours | count with the extra call | rule applied |
|---|---|---|---|
| live, 2 neighbours | 2 | 3 | still survives — no visible error |
| live, 3 neighbours | 3 | 4 | dies when it should live |
| dead, 3 neighbours | 3 | 3 | unchanged — a dead cell adds 0 |
The first row hides the bug and the second reveals it. Live cells die one neighbour too early, and the simulation looks almost right — patterns decay instead of stabilising.
Eight calls, and (r, c) is not one of them.
count += grid.test(r - 1, c - 1);
count += grid.test(r - 1, c);
count += grid.test(r - 1, c + 1);
count += grid.test(r, c - 1);
count += grid.test(r, c + 1); // note: no test(r, c)
count += grid.test(r + 1, c - 1);
count += grid.test(r + 1, c);
count += grid.test(r + 1, c + 1);| offsets | count |
|---|---|
| all nine combinations of −1, 0, +1 | 9 |
| minus the cell itself | 8 |
The gap in the middle of the list is the point. The eight lines are laid out in reading order — top row, middle row, bottom row — precisely so the missing centre is visible on the page.
Prediction
The cell exists but is dead.
try {
if (array[r][c].isOn()) {
return 1;
}
} catch (ArrayIndexOutOfBoundsException e) {
}
return 0;| case | path |
|---|---|
| cell on | return 1 inside the try |
| cell off | ? |
Predict first
What happens when the cell is off?
Correct: The if is false, no exception is thrown, and the method falls through to return 0
Why: Otherwise it skips the catch block and returns 0. The final return 0 serves two different cases — a real dead cell and a cell that does not exist — and that sharing is what makes the method four lines instead of eight.
Definition probe
A 5-row, 10-column grid.
Sort into buckets
Sort each lookup.
Counterexample
Some programmers would insist on explicit bounds checks.
Discussion prompt
Think Java uses an exception for an entirely predictable situation — the edge of a grid. Make the case against, and then say when the book's choice is the better one.
Hint: Exceptions are usually for the exceptional.
Answer:
Against: an edge cell is not an error, it is a normal case, and exceptions are slower than comparisons — noticeably so if they happen on every cell of every generation.
For: the try-catch version is shorter, has no inequalities to get backwards, and keeps working if the rows ever have different lengths. On a small grid the speed difference is invisible.
Both are defensible, and knowing why is the point. Exercise 15.2 changes this method to wrap around into a toroidal grid — and that version needs arithmetic rather than an exception, so the decision is not permanent either.
Section
Section 15.10
Concept
Now we are ready to write update, which gets invoked each time through the simulation loop. It uses the GoL rules to compute the state of the grid after the next time step.
public void update() {
int[][] counts = countNeighbors();
updateGrid(counts);
}| pass | method | reads | writes |
|---|---|---|---|
| 1 | countNeighbors() | the grid | a new counts array |
| 2 | updateGrid(counts) | the counts array | the grid |
The rules of GoL specify that you have to update the cells simultaneously; that is, you have to count the neighbors for all cells before you can update any of them. We do that by traversing the grid twice. Two lines, and they are the whole answer to the simultaneity problem.
Picture it
countNeighbors traverses the cells in the grid and uses countAlive to count the neighbors. The return value is a 2D array of integers with the same size as grid.
Figure (svg): A grid of neighbour counts corresponding to the two-blinker starting position
A second array is the standard answer to simultaneity. Nothing in the counts array depends on anything that has already changed, because at the time it was filled, nothing had.
Worked example
The loop style changes from Lesson 15a's draw, and the reason is stated outright.
private int[][] countNeighbors() {
int rows = grid.numRows();
int cols = grid.numCols();
int[][] counts = new int[rows][cols];
for (int r = 0; r < rows; r++) {
for (int c = 0; c < cols; c++) {
counts[r][c] = countAlive(r, c);
}
}
return counts;
}| needs | draw | countNeighbors |
|---|---|---|
| each element | yes | yes |
| the position r, c | no | yes |
| so the loop is | enhanced | standard |
Allocate a counts array the same size.
Why: new int[rows][cols] — of ints, not Cells, so the elements start at 0 rather than null.
Traverse with r and c.
Why: Standard loops, because the indexes are needed.
Store each count at the matching position.
Why: counts[r][c] = countAlive(r, c);
Return the whole array.
Why: The second pass consumes it.
Verify: Notice counts is a local variable, created fresh each time step and discarded after.
Why: In contrast to the draw method of GridCanvas, which uses enhanced for loops, countNeighbors uses standard for loops. The reason is that, in this example, we need the indexes r and c to store the neighbor counts. That is the clearest statement in the book of when each loop is right.
Prediction
One pass would be shorter.
public void update() {
int[][] counts = countNeighbors();
updateGrid(counts);
}| pass | role |
|---|---|
| countNeighbors | read |
| updateGrid | write |
Predict first
What would go wrong with one pass?
Correct: Cells changed early would corrupt the neighbour counts of cells processed later
Why: You have to count the neighbors for all cells before you can update any of them. With one pass, cell (0,0) counts correctly and everything after it counts against a partly-updated grid — so the result depends on traversal order, which the real rules never do.
Concept
updateGrid uses getCell to select each Cell in the grid, and updateCell to do the update.
private void updateGrid(int[][] counts) {
int rows = grid.numRows();
int cols = grid.numCols();
for (int r = 0; r < rows; r++) {
for (int c = 0; c < cols; c++) {
Cell cell = grid.getCell(r, c);
updateCell(cell, counts[r][c]);
}
}
}| method | private? | static? | why |
|---|---|---|---|
| update | no | no | called from mainloop |
| countNeighbors | yes | no | a helper; reads grid |
| updateGrid | yes | no | a helper; reads grid |
| updateCell | yes | yes | does not depend on grid |
Notice that updateGrid and updateCell are both private, because they are helper methods not intended to be invoked from outside the class. updateCell is also static, because it does not depend on grid. That is Lesson 13b's rule applied without comment — and it is the same distinction that made merge static.
Trap
One pass, counting as you go.
for (int r = 0; r < rows; r++) {
for (int c = 0; c < cols; c++) {
Cell cell = grid.getCell(r, c);
updateCell(cell, countAlive(r, c)); // counts the CHANGED grid
}
}| cell | counts against | correct? |
|---|---|---|
| (0, 0) | the untouched grid | yes |
| (0, 1) | a grid where (0, 0) already changed | no |
| (4, 9) | a grid changed almost everywhere | no |
This compiles, runs, and produces an animation. It is simply not the Game of Life — the pattern evolves according to a rule that depends on traversal order, which no version of Conway's rules does.
Count everything first, into a separate array.
public void update() {
int[][] counts = countNeighbors(); // pass 1: read only
updateGrid(counts); // pass 2: write only
}| pass | touches the grid how |
|---|---|
| countNeighbors | reads only |
| updateGrid | writes only, from counts |
| result | every cell sees the same original grid |
Separating the read pass from the write pass is what simultaneity means in code. The counts array is a snapshot, and the second pass consults the snapshot rather than the grid it is changing.
Prediction
It is an array of ints, not Cells.
int[][] counts = new int[rows][cols];| element type | initial value |
|---|---|
| Cell | null |
| int | ? |
Predict first
What does each element start as?
Correct: 0
Why: Arrays of primitives are initialised to zero, unlike arrays of objects which start as null — the distinction from Lesson 12b. Here it does not matter, since every element is assigned before being read, but it is why counts needs no separate initialisation loop.
Fill the middle
Read everything, then write everything.
Fill in the blanks
public void update() countNeighbors}();
updateGrid(counts);
}
Why: The first call reads the whole grid and writes nothing; the second reads the counts and writes the grid. Keeping the two apart is exactly what makes the cells change simultaneously — and it is why update is two lines rather than a nested loop.
Explain it to yourself
The other helpers are not.
Discussion prompt
countNeighbors and updateGrid are instance methods; updateCell is static. What distinguishes it?
Hint: What does each one need from the object?
Answer:
updateCell does not depend on grid. It takes a Cell and a count as parameters and works entirely on those — so it needs no object.
The other two read grid to get dimensions and cells, so they need a particular Conway object.
The rule from Lesson 13b, unchanged: a method that reads or writes no instance variable should be static. It is also a hint about the method — updateCell is pure rule-application, which is why it is the one method that reads exactly like the three rules.
Section
Section 15.10
Concept
updateCell implements the GoL rules: if the cell is alive, it dies if it has fewer than two or more than three neighbors; if the cell is dead, it comes to life if it has exactly three.
private static void updateCell(Cell cell, int count) {
if (cell.isOn()) {
if (count < 2 || count > 3) {
cell.turnOff();
}
} else {
if (count == 3) {
cell.turnOn();
}
}
}| cell.isOn() | count | action |
|---|---|---|
| true | 0 or 1 | turnOff — underpopulation |
| true | 2 or 3 | nothing — survives |
| true | 4 to 8 | turnOff — overpopulation |
| false | 3 | turnOn — reproduction |
| false | anything else | nothing |
The rules say what changes, so the code only writes changes. A live cell with two neighbours has no branch at all — doing nothing is the correct action, and there is no else for it.
Notation
Line up the English against the Java. Every clause maps to one comparison.
Annotate
||, because both end in turnOff.count == 3 is exact, not >= 3 — a dead cell with four live neighbours stays dead.When code lines up with the rule it implements, checking it is reading rather than reasoning. That is worth arranging deliberately — the same goal as cardMatches in Lesson 14b.
Worked example
Take the horizontal blinker from Lesson 15a and run the whole pipeline on it.
// grid before: row 2 has cells at columns 1, 2, 3
// (all other cells off)| cell | on? | count | rule | after |
|---|---|---|---|---|
| (2, 1) | yes | 1 | fewer than 2 — dies | off |
| (2, 2) | yes | 2 | survives | on |
| (2, 3) | yes | 1 | fewer than 2 — dies | off |
| (1, 2) | no | 3 | exactly 3 — born | on |
| (3, 2) | no | 3 | exactly 3 — born | on |
countNeighbors fills the whole counts array.
Why: Every cell in the grid, read from the unchanged grid.
updateGrid walks the grid again.
Why: Passing each cell and its count to updateCell.
Each cell is decided independently.
Why: Nothing that happened to (2, 1) affects the decision about (2, 2).
The result is a vertical blinker.
Why: Column 2, rows 1 through 3.
Verify: Check (2, 2): its neighbours are (2, 1) and (2, 3), so its count is 2 and it survives.
Why: Now confirm the two-pass structure mattered. In a single pass, (2, 1) would be turned off first, so (2, 2) would count only one live neighbour and die — and the blinker would vanish instead of rotating.
Prediction
Run it through updateCell.
if (cell.isOn()) {
if (count < 2 || count > 3) {
cell.turnOff();
}
}| test | result |
|---|---|
| cell.isOn() | true |
| count < 2 | false |
| count > 3 | ? |
Predict first
What happens?
Correct: It is turned off — overpopulation
Why: Four is more than three, so the second half of the || is true and turnOff runs. A live cell with more than three live neighbors dies, as if by overpopulation — and note that the change takes effect in the grid immediately, which is safe only because the counts were all taken first.
Concept
Now our implementation of the Game of Life is complete. Four classes and about a hundred and fifty lines.
| class | lines, roughly | responsibility |
|---|---|---|
| Cell | 35 | one square: position, state, drawing |
| GridCanvas | 50 | the 2D array, drawing, and test |
| Conway | 60 | the rules, the update, the loop |
| main | 10 | the window |
But more importantly, this example is meant to demonstrate the use of 2D arrays and an object-oriented design that's a little more substantial than in previous chapters. Note how much of it is not the rules: nine lines of rule and everything else machinery.
Trap
Adding a branch for a case that needs no action.
if (cell.isOn()) {
if (count < 2 || count > 3) {
cell.turnOff();
} else {
cell.turnOn(); // it is already on
}
}| case | with the else | correct? |
|---|---|---|
| live, 2 neighbours | turnOn — already on | harmless but pointless |
| what it suggests to a reader | that something happens here | misleading |
| if the rules gain a third state | this line becomes a bug | yes |
It is not wrong today. It says something is happening when nothing is — and a reader has to work out that the branch is a no-op, which is effort spent for nothing.
No branch for no change.
if (cell.isOn()) {
if (count < 2 || count > 3) {
cell.turnOff();
}
} else {
if (count == 3) {
cell.turnOn();
}
}| structure | matches |
|---|---|
| outer if on the current state | every rule begins a live cell or a dead cell |
| inner if on the count | the condition in the rule |
| no else inside | no rule says a cell stays the same |
Code should have a branch for each rule, not for each case. Three rules, three conditions, and the unmentioned cases fall through — which is exactly how the rules are written in English.
Definition probe
The outer if splits on the current state.
Sort into buckets
Sort each case.
Fill the middle
Death has two conditions; birth has one.
Fill in the blanks
if (cell.isOn()) ||} count > 3) ==
} else ___} 3) ___
}
Why: The two death rules are alternatives, so they are joined with ||; birth requires exactly three, so the test is equality rather than >=. A dead cell with four live neighbours stays dead, which is the detail >= would get wrong.
Real world
Nine lines of rule; a hundred and forty of everything else.
Discussion prompt
The Game of Life's rules fit in three sentences, and the program is four classes. Where did all the other code go, and is that ratio typical?
Hint: Count what has nothing to do with the rules.
Answer:
Into representation and presentation: storing the grid, drawing cells, sizing a window, timing an animation, handling the edges. None of it is the rules; all of it is needed to see them.
And the ratio is entirely typical. Most programs are mostly machinery — input, storage, display, error handling — around a small core of actual logic.
Which is why keeping that core separate and readable matters so much. updateCell is nine lines you can check against the rules by eye, and no amount of surrounding machinery makes it harder to read — because none of the machinery is in it.
Comparison
Fill the blanks.
Comparison matrix
| Thread.sleep | array[r][c] | |
|---|---|---|
| exception caught | InterruptedException | ArrayIndexOutOfBoundsException |
| why you wrote the try | the compiler refused otherwise | as a deliberate technique |
| how often it fires | almost never | on every edge cell, every generation |
| the catch block | empty — the pause was cut short, no harm | empty — the cell does not exist, so it is off |
The second column is the interesting one. An exception used as ordinary control flow is unusual, and the book chooses it because eight bounds checks would be longer and easier to get wrong.
Pattern
Simultaneous update: read everything into a snapshot, then write everything from it.
public void update() {
int[][] counts = countNeighbors(); // pass 1: READ the grid
updateGrid(counts); // pass 2: WRITE the grid
}
// pass 1 - needs indexes, so standard loops
for (int r = 0; r < rows; r++) {
for (int c = 0; c < cols; c++) {
counts[r][c] = countAlive(r, c);
}
}
// and the edges need no special case, because test catches
public int test(int r, int c) {
try {
if (array[r][c].isOn()) { return 1; }
} catch (ArrayIndexOutOfBoundsException e) {
// cell doesn't exist
}
return 0;
}| decision | reason |
|---|---|
| two passes | cells must change simultaneously |
| a separate counts array | the snapshot nothing has modified |
| standard loops in pass 1 | the indexes are needed |
| try-catch in test | one handler instead of eight bounds checks |
| updateCell is static | it touches no instance variable |
Check
Work it out before you click.
try {
somethingThatThrowsNullPointer();
} catch (ArrayIndexOutOfBoundsException e) {
// do nothing
}
System.out.println("after");| thrown | caught type |
|---|---|
| NullPointerException | ArrayIndexOutOfBoundsException |
Check your understanding
What happens?
Answer: A
Why: If a different exception occurs during the try block, Java does whatever it would do otherwise, which is probably to display a message and end the program. A catch clause diverts only the type it names, which is precisely why naming a narrow type is safer than catching Exception — an unexpected bug still surfaces.
Check
Work it out before you click.
public int test(int r, int c) {
try {
if (array[r][c].isOn()) {
return 1;
}
} catch (ArrayIndexOutOfBoundsException e) {
}
return 0;
}
// grid is 5 x 10; call test(-1, 4)| index | valid? |
|---|---|
| r = −1 | no |
Check your understanding
What does test(-1, 4) return?
Answer: A
Why: If either index is out of bounds, the array lookup throws an exception, but the catch block ignores it. Then test resumes and returns 0. That is the design decision that non-existent cells count as off — and it is what lets countAlive make all eight calls without a single bounds check.
Check
Work it out before you click.
public void update() {
int[][] counts = countNeighbors();
updateGrid(counts);
}| pass | reads | writes |
|---|---|---|
| 1 | the grid | counts |
| 2 | counts | the grid |
Check your understanding
Why can't countNeighbors and updateGrid be merged into one loop?
Answer: A
Why: The rules of GoL specify that you have to update the cells simultaneously; that is, you have to count the neighbors for all cells before you can update any of them. Merging the passes makes the outcome depend on traversal order — the first cell would be right and everything after it counted against a partly-updated grid.
Real world
Reading from one copy while writing to another is the standard answer whenever an update must appear simultaneous.
Discussion prompt
The counts array is a snapshot nothing has modified. Where else does software keep two copies for exactly this reason?
Hint: Screens, backups, deployments.
Answer:
Graphics does it constantly — a frame is drawn into an off-screen buffer and then swapped in whole, so the viewer never sees a half-drawn image. It is literally called double buffering.
Physical simulations keep a current and a next state for the same reason as this grid; databases give each transaction a consistent snapshot; and a blue-green deployment builds the new version alongside the old and switches once.
The shape is always the same: never let the thing you are computing from be the thing you are changing. Once you have seen it in the Game of Life, you will recognise it everywhere — and recognise the bug when someone has skipped it.
Commit first
Commit to an answer and to your confidence.
Predict first
Why does test use a try-catch rather than checking whether r and c are in bounds?
Correct: One handler covers all four edges, with no inequalities to get backwards — at the cost of some speed
Why: Explicit checks would need four comparisons — r < 0, r >= numRows(), c < 0, c >= numCols() — and every one is a chance to write > where >= belongs. The try-catch has none. Because test handles out-of-bounds exceptions, countAlive works for any values of r and c, which is what lets it make all eight calls unguarded. The cost is real: throwing an exception is slower than comparing, and on an edge cell five of the eight lookups throw. On a small grid you will not notice, and the fourth option is not the reason — explicit checks can handle ragged arrays too, just with more care.
Explain it
Two minutes, out loud.
Discussion prompt
A classmate's Game of Life almost works — patterns decay instead of blinking. They updated each cell as they counted it. Explain what is wrong and what to change.
Hint: Which cell is right, and which are wrong?
Answer:
The rules say all cells change at once. If you update as you go, the first cell counts against the original grid and is right — but every cell after it counts against a grid you have already been changing.
So a blinker dies instead of rotating: by the time you reach the centre cell, you have already turned off the neighbour to its left, so it sees one live neighbour instead of two.
The fix is two passes: fill a separate counts array from the untouched grid, then walk the grid again and apply the rules from the counts. The second array is not an optimisation — it is what simultaneity means in code.
Exit ticket
One question before you close the deck.
Predict first
What does a try-catch statement do when no exception occurs?
Correct: The try block runs to completion, the catch block is skipped, and the program continues
Why: The syntax is similar to an if-else statement, and the logic is, too. Java runs the try block; if the named exception occurs it jumps to the catch block; if nothing goes wrong the catch block does not run at all. And if a different exception occurs, Java does what it would have done anyway — probably display a message and end the program. Exactly one of the two blocks completes, which is why test's return 0 sits outside both: it is the one line that must run in every case except a live cell.
Connect it up
One page, from memory.
Draw it
Draw a 5×5 grid and write in each cell how many neighbours it actually has — 8 in the interior, 5 on an edge, 3 in a corner. Beside it write the test method and mark which line runs for a live cell, a dead cell and a cell off the edge. Then draw the two passes of update as two boxes, labelling what each reads and writes, and trace one time step of a blinker through both. Finish by writing updateCell and drawing a line from each of its conditions to the English rule it implements.
Recap
Four sections that make the grid move, and introduce exceptions twice for two different reasons.
| if you remember one thing | it is this |
|---|---|
| about exceptions | catch the specific type, around the specific line |
| about the grid edges | one handler beats eight bounds checks |
| about simultaneity | read into a snapshot, then write from it |
Want this taught 1-on-1? Alexander tutors Java — $55/session, free consultation.