The Simulation Loop, Exceptions, and Counting Neighbors

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

What this lesson covers

The lesson, slide by slide

1. The Simulation Loop, Exceptions, and Counting Neighbors

Title

Think Java 2e · Chapter 15 · Arrays of Arrays

Sections 15.7-15.10 · pp. 257-263

2. What you will be able to do

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.

3. Retrieve before you read

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.

4. An exception you handle rather than avoid

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.

5. The simulation loop

Section

Section 15.7

6. Update, repaint, pause, repeat

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
    }
}
linedoes
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.

7. Why each line is there

Notation

Every one of the three does something the animation would fail without.

Annotate

  • update changes the model; repaint changes the view. Without update, nothing moves; without repaint, the window keeps showing the old picture.
  • repaint asks; it does not draw. The system calls paint when it is ready, which is why no Graphics object is needed.
  • Without the sleep the simulation still runs — far too fast to watch, and using all the processor it can get.
  • 500 milliseconds is a choice, not a rule. A smaller number gives a faster animation.
  • 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.

8. The compiler error

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 farInterruptedException
when you find outat run timeat compile time
examplesarray index out of bounds, null pointersleep being interrupted
can you ignore it?yes, until it happensno — 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.

9. What does Thread.sleep(500) do?

Prediction

The units matter.

Thread.sleep(500);
argumentunit
500milliseconds

Predict first

How long does the program pause?

  • Half a second
  • 500 seconds
  • Five seconds
  • Half a minute

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.

10. Why the loop lives in Conway

Concept

mainloop is private and belongs to Conway, not to GridCanvas — and the split follows Lesson 15a's division of responsibility.

classresponsible for
Cellits own state and how to draw itself
GridCanvasthe 2D array, and drawing all the cells
Conwaythe 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.

11. A loop with no pause

Trap

The trap

Without sleep, the animation is unwatchable.

while (true) {
    update();
    grid.repaint();
    // no pause
}
consequencedetail
thousands of generations per secondyou see a blur, or nothing
repaint requests pile upthe system cannot keep pace
the processor runs flat outfor 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.

The fix

Pause between time steps, and choose the interval.

while (true) {
    update();
    grid.repaint();
    try {
        Thread.sleep(500);
    } catch (InterruptedException e) {
        // do nothing
    }
}
sleepgenerations per second
5002
10010
10001

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.

12. Why repaint rather than paint?

Prediction

Both eventually draw the grid.

grid.repaint();      // not grid.paint(???)
methodparameter
paint(Graphics g)needs a Graphics object
repaint()none

Predict first

What is the reason?

  • repaint needs no Graphics object — the system supplies one when it calls paint
  • paint is private
  • repaint draws faster
  • paint would draw twice

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.

13. Which class does each job?

Definition probe

Model, view and rules.

Sort into buckets

Sort each responsibility.

Cell
knowing whether one cell is on
GridCanvas
drawing every cell
Conway
applying the Game of Life rules; the timing of the animation
cell
A cell owns its own state and its own appearance, and nothing else.
grid
GridCanvas owns the 2D array and the drawing — general machinery that would serve any grid simulation.
conway
Conway owns everything specific to the Game of Life: the rules, the update, and the loop that drives them.

14. Why is while (true) acceptable here?

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.

15. Exception handling

Section

Section 15.8

16. try and catch

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 blockthen
no exceptionthe catch block does not run
an InterruptedExceptionthe catch block runs
a different exceptionJava 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.

17. The three paths through try-catch

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.

18. An empty catch block, on purpose

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
}
optionwhat the catch block would do
ignore itnothing — the loop continues
report itprint a message
give upend the program
recoverprompt 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.

19. What runs when there is no exception?

Prediction

The try block succeeds.

try {
    Thread.sleep(500);
} catch (InterruptedException e) {
    System.out.println("interrupted");
}
System.out.println("done");
blockruns?
tryyes
catch?

Predict first

What is printed?

  • Just done — the catch block does not run
  • interrupted, then done
  • Just interrupted
  • Nothing

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.

20. Two kinds of exception

Concept

The exceptions you have met behave differently from this one, and the difference is about when you find out.

the ones so farInterruptedException
examplesArrayIndexOutOfBounds, NullPointerInterruptedException, FileNotFound
discoveredwhen the program runswhen it compiles
must you handle it?noyes — or it will not compile
typical causea bug in your codesomething 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.

21. Catching everything and doing nothing

Trap

The 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 swallowedconsequence
InterruptedExceptionfine — intended
NullPointerException in updatesilently skipped, forever
ArrayIndexOutOfBoundsthe simulation quietly stops working
any bug you introduce laterinvisible

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.

The fix

Catch the specific exception, around the specific line.

update();
grid.repaint();
try {
    Thread.sleep(500);
} catch (InterruptedException e) {
    // do nothing
}
choiceeffect
only sleep is inside the trybugs in update still crash loudly
only InterruptedException is caughteverything else propagates
the catch is emptyand 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.

22. An exception the catch does not name

Prediction

The try block throws a NullPointerException.

try {
    somethingThatThrowsNullPointer();
} catch (InterruptedException e) {
    // do nothing
}
throwncaught here?
InterruptedExceptionyes
NullPointerException?

Predict first

What happens?

  • It is not caught — Java does what it would otherwise, probably ending the program
  • It is caught, since all exceptions are the same
  • The program continues silently
  • A compile error

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.

23. Handle the interruption

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.

24. When is an empty catch block wrong?

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.

25. Counting neighbours

Section

Section 15.9

26. Let the exception handle the edges

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, cin bounds?cell on?returns
2, 3yesyes1
2, 4yesno0
-1, 3no—0 — the exception is caught
99, 3no—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.

27. Reading test carefully

Notation

Four small decisions, and every one of them matters.

Annotate

  • The 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.
  • The catch block is empty — the comment says why: cell doesn't exist, and a non-existent cell counts as off.
  • 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.
  • So the non-existent cells around the perimeter are considered to be off — which is a design decision about what lies beyond the grid, made by three lines of code.

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.

28. Eight calls, no special cases

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;
}
callrelative 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.

29. How many neighbours does a corner cell have?

Prediction

Cell (0, 0) of any grid.

countAlive(0, 0);
// eight calls, five of them out of bounds
offsetpositionin bounds?
(−1, −1), (−1, 0), (−1, 1)aboveno
(0, −1)leftno
(0, 1), (1, 0), (1, 1)right and belowyes
(1, −1)below leftno

Predict first

How many of the eight lookups can return 1?

  • 3
  • 5
  • 8
  • 0

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.

30. The alternative, for comparison

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-catchexplicit checks
linesfewermore
chances to get an inequality wrongnonefour
works if the grid is raggedyesonly with more care
speed when out of boundsslower — exceptions costfaster

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.

31. Counting the cell itself

Trap

The trap

A ninth call, for (r, c).

count += grid.test(r, c);      // the cell ITSELF - not a neighbour
celltrue neighbourscount with the extra callrule applied
live, 2 neighbours23still survives — no visible error
live, 3 neighbours34dies when it should live
dead, 3 neighbours33unchanged — 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.

The fix

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);
offsetscount
all nine combinations of −1, 0, +19
minus the cell itself8

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.

32. What does test return for an off cell?

Prediction

The cell exists but is dead.

try {
    if (array[r][c].isOn()) {
        return 1;
    }
} catch (ArrayIndexOutOfBoundsException e) {
}
return 0;
casepath
cell onreturn 1 inside the try
cell off?

Predict first

What happens when the cell is off?

  • The if is false, no exception is thrown, and the method falls through to return 0
  • The catch block runs
  • It returns 1
  • It throws an exception

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.

33. In bounds or caught?

Definition probe

A 5-row, 10-column grid.

Sort into buckets

Sort each lookup.

in bounds
test(0, 0); test(4, 9)
throws, caught, returns 0
test(-1, 0); test(5, 0)
ok
Both indexes fall inside the array, so the Cell exists and its state decides the return value.
ex
At least one index is outside — negative, or at least as large as the dimension — so the lookup throws and the catch block treats the cell as off.

34. Is using exceptions for this good style?

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.

35. Updating the grid

Section

Section 15.10

36. Two passes, so that everything changes at once

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);
}
passmethodreadswrites
1countNeighbors()the grida new counts array
2updateGrid(counts)the counts arraythe 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.

37. The counts array

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.

38. countNeighbors, and why it uses standard loops

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;
}
needsdrawcountNeighbors
each elementyesyes
the position r, cnoyes
so the loop isenhancedstandard

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.

39. Why does update need two passes?

Prediction

One pass would be shorter.

public void update() {
    int[][] counts = countNeighbors();
    updateGrid(counts);
}
passrole
countNeighborsread
updateGridwrite

Predict first

What would go wrong with one pass?

  • Cells changed early would corrupt the neighbour counts of cells processed later
  • It would be too slow
  • The counts array would be the wrong size
  • Nothing — one pass would work

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.

40. updateGrid and updateCell

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]);
        }
    }
}
methodprivate?static?why
updatenonocalled from mainloop
countNeighborsyesnoa helper; reads grid
updateGridyesnoa helper; reads grid
updateCellyesyesdoes 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.

41. Updating from the grid instead of the counts

Trap

The 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
    }
}
cellcounts againstcorrect?
(0, 0)the untouched gridyes
(0, 1)a grid where (0, 0) already changedno
(4, 9)a grid changed almost everywhereno

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.

The fix

Count everything first, into a separate array.

public void update() {
    int[][] counts = countNeighbors();   // pass 1: read only
    updateGrid(counts);                  // pass 2: write only
}
passtouches the grid how
countNeighborsreads only
updateGridwrites only, from counts
resultevery 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.

42. What are the elements of the counts array initially?

Prediction

It is an array of ints, not Cells.

int[][] counts = new int[rows][cols];
element typeinitial value
Cellnull
int?

Predict first

What does each element start as?

  • 0
  • null
  • undefined
  • 1

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.

43. The two-pass update

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.

44. Why is updateCell static?

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.

45. The rules, in code

Section

Section 15.10

46. updateCell is the three rules

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()countaction
true0 or 1turnOff — underpopulation
true2 or 3nothing — survives
true4 to 8turnOff — overpopulation
false3turnOn — reproduction
falseanything elsenothing

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.

47. Three rules, three lines of code

Notation

Line up the English against the Java. Every clause maps to one comparison.

Annotate

  • The two death rules share a branch, joined with ||, because both end in turnOff.
  • The outer if splits on the cell's current state, which is what the rules do too — every rule begins a live cell or a dead cell.
  • There is no rule for surviving, so there is no code for it. A live cell with 2 or 3 neighbours falls through both conditions untouched.
  • count == 3 is exact, not >= 3 — a dead cell with four live neighbours stays dead.
  • Nine lines for the entire behaviour of the Game of Life. Everything else in the program is machinery for running them.

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.

48. One complete time step

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)
cellon?countruleafter
(2, 1)yes1fewer than 2 — diesoff
(2, 2)yes2surviveson
(2, 3)yes1fewer than 2 — diesoff
(1, 2)no3exactly 3 — bornon
(3, 2)no3exactly 3 — bornon

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.

49. A live cell with four neighbours

Prediction

Run it through updateCell.

if (cell.isOn()) {
    if (count < 2 || count > 3) {
        cell.turnOff();
    }
}
testresult
cell.isOn()true
count < 2false
count > 3?

Predict first

What happens?

  • It is turned off — overpopulation
  • It stays on
  • It is turned on again
  • Nothing happens until the next step

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.

50. The finished program

Concept

Now our implementation of the Game of Life is complete. Four classes and about a hundred and fifty lines.

classlines, roughlyresponsibility
Cell35one square: position, state, drawing
GridCanvas50the 2D array, drawing, and test
Conway60the rules, the update, the loop
main10the 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.

51. Writing an else for survival

Trap

The 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
    }
}
casewith the elsecorrect?
live, 2 neighboursturnOn — already onharmless but pointless
what it suggests to a readerthat something happens heremisleading
if the rules gain a third statethis line becomes a bugyes

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.

The fix

No branch for no change.

if (cell.isOn()) {
    if (count < 2 || count > 3) {
        cell.turnOff();
    }
} else {
    if (count == 3) {
        cell.turnOn();
    }
}
structurematches
outer if on the current stateevery rule begins a live cell or a dead cell
inner if on the countthe condition in the rule
no else insideno 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.

52. Which branch of updateCell?

Definition probe

The outer if splits on the current state.

Sort into buckets

Sort each case.

turnOff is called
live, 1 neighbour; live, 5 neighbours
turnOn is called
dead, 3 neighbours
nothing is called
dead, 2 neighbours
off
A live cell with fewer than two or more than three neighbours dies.
on
A dead cell with exactly three live neighbours comes to life.
none
No rule applies, so no method is called — the cell simply stays as it is.

53. Implement the rules

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.

54. Why is so little of the program the rules?

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.

55. Two uses of try-catch in one chapter

Comparison

Fill the blanks.

Comparison matrix

Thread.sleeparray[r][c]
exception caughtInterruptedExceptionArrayIndexOutOfBoundsException
why you wrote the trythe compiler refused otherwiseas a deliberate technique
how often it firesalmost neveron every edge cell, every generation
the catch blockempty — the pause was cut short, no harmempty — 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.

56. The pattern to carry away

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;
}
decisionreason
two passescells must change simultaneously
a separate counts arraythe snapshot nothing has modified
standard loops in pass 1the indexes are needed
try-catch in testone handler instead of eight bounds checks
updateCell is staticit touches no instance variable

57. Check: try-catch

Check

Work it out before you click.

try {
    somethingThatThrowsNullPointer();
} catch (ArrayIndexOutOfBoundsException e) {
    // do nothing
}
System.out.println("after");
throwncaught type
NullPointerExceptionArrayIndexOutOfBoundsException

Check your understanding

What happens?

  • A. The exception is not caught — the program displays a message and ends, so "after" is never printed (correct)
  • B. The catch block runs and "after" is printed
  • C. "after" is printed and the exception is ignored
  • D. A compile error

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.

Why B tempts people
A NullPointerException is not an ArrayIndexOutOfBoundsException, so this catch does not apply.
Why C tempts people
Nothing ignores an uncaught exception; execution does not continue past it.
Why D tempts people
The code compiles fine — these are run-time exceptions.

58. Check: test

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)
indexvalid?
r = −1no

Check your understanding

What does test(-1, 4) return?

  • A. 0 — the lookup throws, the catch ignores it, and the method falls through to return 0 (correct)
  • B. 1
  • C. It throws an ArrayIndexOutOfBoundsException
  • D. null

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.

Why B tempts people
1 is returned only for a cell that exists and is on.
Why C tempts people
The exception is thrown but caught here, so it never reaches the caller.
Why D tempts people
The method's return type is int; it cannot return null.

59. Check: two passes

Check

Work it out before you click.

public void update() {
    int[][] counts = countNeighbors();
    updateGrid(counts);
}
passreadswrites
1the gridcounts
2countsthe grid

Check your understanding

Why can't countNeighbors and updateGrid be merged into one loop?

  • A. Because a cell changed early would corrupt the neighbour counts of every cell processed after it (correct)
  • B. Because you cannot read and write the same array in one loop
  • C. Because the two loops traverse in different orders
  • D. Because counts and grid have different sizes

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.

Why B tempts people
Java allows that freely; the problem is the meaning, not the mechanics.
Why C tempts people
Both traverse in the same row-major order.
Why D tempts people
They are deliberately the same size — one count per cell.

60. Double buffering, everywhere

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.

61. How sure are you?

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?

  • One handler covers all four edges, with no inequalities to get backwards — at the cost of some speed
  • Because Java has no way to check array bounds
  • Because exceptions are faster than comparisons
  • Because the grid might be ragged, and only exceptions work then

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.

62. Explain it to someone else

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.

63. Exit ticket

Exit ticket

One question before you close the deck.

Predict first

What does a try-catch statement do when no exception occurs?

  • The try block runs to completion, the catch block is skipped, and the program continues
  • Both blocks run
  • The catch block runs first
  • The program pauses until an 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.

64. Draw the whole lesson

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.

65. Recap

Recap

Four sections that make the grid move, and introduce exceptions twice for two different reasons.

if you remember one thingit is this
about exceptionscatch the specific type, around the specific line
about the grid edgesone handler beats eight bounds checks
about simultaneityread into a snapshot, then write from it

Sources

  1. 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
  2. The Java Tutorials — Catching and Handling Exceptions
  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