Two-Dimensional Arrays and the Game of Life Grid

Conway's Game of Life, its three rules, and the classes that draw it: a Cell that knows its own coordinates and state, a two-dimensional array — which in Java is really an array of arrays — and a GridCanvas that is a Canvas and has a grid of cells. Follows Think Java 2e, Chapter 15 (Arrays of Arrays), Sections 15.1-15.6, pp. 249-257, cross-referenced against The Java Tutorials — Arrays (multidimensional).

Subject: Java · 65 slides · code lesson

Open the interactive version of this deck

What this lesson covers

The lesson, slide by slide

1. Two-Dimensional Arrays and the Game of Life Grid

Title

Think Java 2e · Chapter 15 · Arrays of Arrays

Sections 15.1-15.6 · pp. 249-257

2. What you will be able to do

Objectives

This lesson follows Think Java 2e, Chapter 15 (Arrays of Arrays), Sections 15.1-15.6, pp. 249-257. Everything on these slides can be checked against those pages.

1. State the three rules of the Game of Life and predict a few time steps by hand.

2. Declare and populate a two-dimensional array, and explain what array[r][c] actually does.

3. Explain row-major order and why numRows and numCols are written differently.

4. Traverse a 2D array with nested loops, both standard and enhanced.

5. Extend Canvas and provide draw and paint methods.

6. Choose accessor names that suit the problem rather than the field.

3. Retrieve before you read

Warm-up

Three things you already have.

Discussion prompt

From Lesson 7a: what is array.length, and what are the elements of new Cell[5]? From Lesson 12a: what does final on an instance variable mean? And from Lesson 14a: what do IS-A and HAS-A mean?

Hint: A field, not a method. Five nulls. One assignment. And two sentences.

Answer:

length is a field, not a method — no parentheses. new Cell[5] gives five nulls, since the elements are object variables. final allows one initialisation, in the constructor. And IS-A means extends; HAS-A means an instance variable.

All four appear in this lesson. The last three chapters of this book use 2D graphics to illustrate more advanced object-oriented concepts — and the design vocabulary from Chapter 14 is what makes GridCanvas describable in one sentence.

4. A zero-player game

Concept

The Game of Life was developed by John Conway and popularized in 1970 in Martin Gardner's column in Scientific American. Conway calls it a zero-player game because no players are needed to choose strategies or make decisions. After you set up the initial conditions, you watch the game play itself.

Figure (svg): A five by five grid showing a glider pattern of five live cells in the Game of Life

Downey & Mayfield, Think Java, 2nd edition (Green Tea Press / O'Reilly, 2020) — Think Java 2e, Chapter 15 (Arrays of Arrays), Sections 15.1-15.6, pp. 249-257 — Chapter 15 opens on printed page 249.

5. The rules

Section

Section 15.1

6. Three rules, applied to every cell at once

Concept

The game proceeds in time steps, during which each cell interacts with its neighbors in the eight adjacent cells. At each time step, the following rules are applied.

// A live cell with FEWER THAN TWO live neighbors
//    dies, as if by underpopulation.
//
// A live cell with MORE THAN THREE live neighbors
//    dies, as if by overpopulation.
//
// A dead cell with EXACTLY THREE live neighbors
//    becomes a live cell, as if by reproduction.
cell islive neighboursnext state
alive0 or 1dead — underpopulation
alive2 or 3alive
alive4 to 8dead — overpopulation
deadexactly 3alive — reproduction
deadanything elsedead

Each cell is either alive or dead; the color of the cell indicates its state. Note that the rules never mention how a cell got to be alive — only its current state and its neighbour count matter, which is what makes the whole thing computable in one pass.

7. Consequences worth noticing

Notation

Notice some consequences of these rules. Each one is a prediction you can check by hand.

Annotate

  • A lone cell dies immediately — the rules are unforgiving about isolation.
  • An empty grid stays empty forever, so the game has a fixed point at zero.
  • Four in a square is stable: each of the four has exactly two live neighbours, so each survives, and no dead cell has three.
  • Most simple starting configurations either die out quickly or reach a stable configuration.
  • But there are a few starting conditions that display remarkable complexity. The R-pentomino starts with five cells, runs for 1,103 time steps, and ends with 116 live cells.

That turns out to be more interesting than it sounds. Three rules, no randomness, no input — and behaviour nobody can predict without running it.

8. The Blinker

Worked example

Another initial configuration is shown in Figure 15.2. Three cells in a row, and it never settles.

Figure (svg): Two grids side by side showing three horizontal cells becoming three vertical cells

celllive neighboursrule appliedbecomes
the centre2alive with 2 — survivesalive
the left cell1alive with fewer than 2dead
the right cell1samedead
directly above centre3dead with exactly 3alive
directly below centre3samealive

If you start with three horizontal cells, the center cell lives

Why: it has two live neighbours.

the left and right cells die

Why: each has only one.

and the top and bottom cells come to life.

Why: Each has exactly three live neighbours — the whole row.

The result after the first time step is three vertical cells.

Why: And the next step turns them back.

Verify: Apply the rules to the vertical configuration and confirm you get the horizontal one back.

Why: We're back where we started, and the cycle repeats forever. Patterns like this are called periodic, because they repeat after a period of two or more time steps. But they are also considered stable, because the total number of live cells doesn't grow over time.

9. A live cell with two live neighbours

Prediction

Check every rule.

// dies if fewer than 2
// dies if more than 3
// a dead cell comes alive at exactly 3
ruleapplies?
fewer than 2no — it has 2
more than 3no

Predict first

What happens to it?

  • It stays alive — no rule removes it
  • It dies
  • It comes to life
  • It depends on which neighbours they are

Correct: It stays alive — no rule removes it

Why: The rules only say when a live cell dies: fewer than two neighbours or more than three. Two and three are the survival counts, and nothing about which neighbours matters — only how many. The rules are purely a function of state and count.

10. What the program has to do

Concept

The rules dictate the structure of the code before a line is written.

the rules requirewhich means the program needs
a grid of cells with two statesa Cell class and a 2D array
counting eight neighboursa way to read a cell at (r, c)
cells on the edge have fewerhandling out-of-bounds lookups
all cells update simultaneouslycount everything before changing anything
time stepsa loop with a pause

In the following sections, we'll implement the Game of Life in Java. We'll first implement the cells, then the grid of cells, and finally the game itself. Bottom-up design, exactly as Lesson 14b named it — the rules already list the pieces.

11. Updating cells one at a time

Trap

The trap

Changing a cell before its neighbours have been counted.

for each cell {
    int n = countAlive(r, c);
    updateCell(cell, n);        // changes the grid immediately
}
cellcounts neighbours insees
(0, 0)the original gridcorrect
(0, 1)a grid already changed at (0, 0)wrong
(0, 2)changed at (0, 0) and (0, 1)worse

The first cell is right and everything after it is contaminated. The pattern that results is not the Game of Life — it is a different game with the same rules and the wrong timing.

The fix

Count everything, then update everything.

int[][] counts = countNeighbors();   // pass 1: read only
updateGrid(counts);                  // pass 2: write only
passreadswrites
countNeighborsthe grida separate counts array
updateGridthe 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. Two passes, and the counts array is what makes simultaneity possible — Lesson 15b writes both.

12. Alive or dead next step?

Definition probe

Apply the rules to each case.

Sort into buckets

Sort each cell by its next state.

alive next step
alive, 3 live neighbours; dead, 3 live neighbours
dead next step
alive, 1 live neighbour; dead, 2 live neighbours
on
Either a live cell with two or three neighbours (it survives) or a dead cell with exactly three (reproduction).
off
Either a live cell with too few or too many neighbours, or a dead cell without exactly three — the only count that revives one.

13. What happens to a single live cell?

Prediction

One cell, no neighbours.

// a live cell with fewer than two live neighbors dies
celllive neighbours
the only live cell0

Predict first

What happens?

  • It dies — zero is fewer than two
  • It survives — nothing is killing it
  • It reproduces
  • It stays the same forever

Correct: It dies — zero is fewer than two

Why: If you start with a single live cell, it dies. The underpopulation rule catches zero as well as one, and since no dead cell can have three live neighbours in an otherwise empty grid, nothing comes back — the grid stays empty forever.

14. Why is a square stable?

Socratic

Four cells in a two-by-two block never change.

Discussion prompt

Work out the neighbour count for one cell of the square, and for a dead cell touching it. Why does nothing happen?

Hint: Count carefully — diagonals count too.

Answer:

In a 2×2 block every cell touches the other three — two orthogonally and one diagonally — so each has exactly three live neighbours.

Three is a survival count, so all four live on. And every dead cell adjacent to the block touches at most two of them, so none reaches three.

Nothing dies and nothing is born, so the configuration is fixed. The Game of Life's stability comes from exact arithmetic, not from anything resembling inertia — which is why predicting it by eye is so unreliable.

15. The Cell class

Section

Section 15.2

16. Three constants and one variable

Concept

When drawing a cell, we'll need to know its location on the screen and size in pixels. To represent the location we use the x and y coordinates of the upper-left corner, and for the size an integer.

public class Cell {
    private final int x;
    private final int y;
    private final int size;
    private int state;

    public Cell(int x, int y, int size) {
        this.x = x;
        this.y = y;
        this.size = size;
        this.state = 0;
    }
}
fieldfinal?why
xyesa cell never moves
yyessame
sizeyesa cell never changes size
statenoit changes every time step

Notice that x, y, and size are constants. Once the cell is created, we don't want it to move or change size. But state can and should change, so it is not a constant. That is Lesson 12a's final used selectively — three fields protected, one deliberately left open.

17. Why state is an int, not a boolean

Notation

A cell is alive or dead. That is exactly two values, and the book still uses an integer.

Annotate

  • A boolean would fit today. Two states, two values — nothing is lost right now.
  • An int extends. A game with three or four states needs no rewrite of the field, only of the code that interprets it.
  • And an int can index an array, which is exactly what COLORS[state] does — a boolean cannot.
  • This is Lesson 12a's encoding argument again: integers compare, index and extend; the cost is that 1 does not read as alive.
  • The cost is paid by the accessor names. isOn() and turnOn() restore the readability the encoding took away.

It's good practice to design classes to be reusable — but notice this is a judgement, not a law. The extra generality is nearly free here, which is what makes it worth taking.

18. Drawing a cell

Worked example

The following method draws a cell. Like the paint method in Appendix C, it takes a graphics context as a parameter.

public static final Color[] COLORS = {Color.WHITE, Color.BLACK};

public void draw(Graphics g) {
    g.setColor(COLORS[state]);
    g.fillRect(x + 1, y + 1, size - 1, size - 1);
    g.setColor(Color.LIGHT_GRAY);
    g.drawRect(x, y, size, size);
}
linedraws
COLORS[state]white if dead, black if alive
fillRect(x + 1, y + 1, size - 1, size - 1)the filled interior, inset by a pixel
setColor(LIGHT_GRAY)the border colour
drawRect(x, y, size, size)a light-gray outline

The state selects a colour.

Why: The draw method uses the state of the cell to select a color from an array of Color objects.

COLORS is a class variable.

Why: public static final Color[] — Lesson 12a's decoding array, in a new setting: state 0 to white, state 1 to black.

Fill, then outline.

Why: Then it uses fillRect to draw the center of the cell and drawRect to draw a light-gray border.

Note the +1 and −1.

Why: The interior is inset by one pixel so the border remains visible.

Verify: Change COLORS to {Color.YELLOW, Color.BLUE} and confirm every cell's colour changes with no other edit.

Why: The array is the mapping from state to appearance, in one place. Adding a third state means adding a third colour and nothing else — which is exactly the reusability the int was chosen for.

19. Why is state not final?

Prediction

The other three fields are.

private final int x;
private final int y;
private final int size;
private int state;
fieldchanges during the game?
x, y, sizeno
stateyes, every time step

Predict first

What is the reason?

  • state changes every time step, and final would forbid every assignment after the constructor
  • final does not work on ints
  • state is private, so final would be redundant
  • final fields cannot be read from other methods

Correct: state changes every time step, and final would forbid every assignment after the constructor

Why: Once the cell is created, we don't want it to move or change size. But state can and should change, so it is not a constant. final allows exactly one initialisation, which is right for the three geometry fields and wrong for the one thing the simulation exists to change.

20. Accessors named for the problem

Concept

We also need methods to get and set the cell's state. We could just provide getState and setState, but the code will be more readable if we provide methods customized for the Game of Life.

public boolean isOff() {
    return state == 0;
}

public boolean isOn() {
    return state == 1;
}

public void turnOff() {
    state = 0;
}

public void turnOn() {
    state = 1;
}
genericcustomisedreads as
getState() == 1isOn()if the cell is on
getState() == 0isOff()if the cell is off
setState(1)turnOn()turn the cell on
setState(0)turnOff()turn the cell off

The encoding is now invisible to callers. Nothing outside Cell needs to know that alive is 1 — and if (cell.isOn()) reads better than if (cell.getState() == 1) at every one of the dozens of places it appears. That is Lesson 13b's wrapper argument, applied to accessors.

21. Exposing the raw state

Trap

The trap

getState and setState spread the encoding everywhere.

if (cell.getState() == 1) { ... }
cell.setState(0);
cell.setState(2);              // a state that does not exist
problemconsequence
callers compare against 1the encoding is copied into every call site
setState takes any intnothing stops state 2 or −5
COLORS[state]an invalid state crashes the drawing code

setState(2) compiles, and the failure appears later inside draw as an ArrayIndexOutOfBoundsException on COLORS[state] — a long way from the line that caused it.

The fix

Methods that can only produce valid states.

public void turnOn()  { state = 1; }
public void turnOff() { state = 0; }
public boolean isOn() { return state == 1; }
guaranteehow
state is always 0 or 1only these methods assign it
COLORS[state] is always validfollows from the above
callers never see the encodingthey say on and off

Four small methods buy an invariant the class can rely on. This is the same reasoning as CardCollection refusing to wrap set in Lesson 14a: a class's guarantees come from what it does not offer.

22. final or not?

Definition probe

Ask whether the value ever changes after construction.

Sort into buckets

Sort each field of Cell.

final
x; size; COLORS
not final
state
fin
The value is fixed once set — a cell never moves or resizes, and the colour table never changes — so final records and enforces that.
var
It changes as the program runs, which is the whole point of the simulation.

23. Customised accessors

Fill the middle

Hide the encoding behind names from the game.

Fill in the blanks

public boolean isOn() ==} 1;
}

public void turnOn() =} 1;
}

Why: One is a comparison and one is an assignment — the distinction from Lesson 2a, in two adjacent methods. Together they keep the fact that alive is 1 inside the Cell class, so nothing else in the program compares against a bare integer.

24. Why COLORS rather than an if?

Explain it to yourself

if (state == 1) g.setColor(BLACK); else g.setColor(WHITE); would work.

Discussion prompt

COLORS[state] replaces a two-branch if. What does the array buy, and when would the if be better?

Hint: How many states might there be?

Answer:

The array scales. A third state needs one more element; the if needs another branch, and a fourth needs another again.

It also puts the whole state-to-colour mapping in one visible line, which is easier to check than a chain of conditions — the same argument as the RANKS array in Lesson 12a.

The if would be better if the colours were computed rather than looked up — a gradient by neighbour count, say. A lookup table is for a fixed small set of cases; a condition is for a rule.

25. Two-dimensional arrays

Section

Section 15.3

26. An array of arrays

Concept

To represent a grid of cells, we can use a multidimensional array. To create a 2D array, we specify the number of rows and columns.

int rows = 4;
int cols = 3;
Cell[][] array = new Cell[rows][cols];
expressionis
arrayan array of 4 rows
array[0]a row — itself an array of 3 Cells
array[0][2]one Cell
all elements initiallynull

multidimensional array — An array with more than one dimension; a 2D array is an array of arrays.

row-major order — Storing data in a 2D array, first by rows and then by columns.

In Java, a 2D array is really an array of arrays. You can think of it as an array of rows, where each row is an array. That one sentence explains the syntax, the two length expressions, and why rows come first.

27. Rows first, then columns

Picture it

When we write array[r][c], Java uses the first index to select a row and the second index to select an element from the row.

Figure (svg): A four by three grid with row and column indexes showing how array indexes map to positions

This way of representing 2D data is known as row-major order. It is a convention, not a law of arrays — but it is Java's, and getting the two indexes the wrong way round is the single most common 2D-array bug.

28. Filling the array with Cells

Worked example

The array starts full of nulls, exactly as in Lesson 12b — only now there are two dimensions of them.

for (int r = 0; r < rows; r++) {
    int y = r * size;
    for (int c = 0; c < cols; c++) {
        int x = c * size;
        array[r][c] = new Cell(x, y, size);
    }
}
rcy = r * sizex = c * sizecell at
0000(0, 0) pixels
01010(10, 0)
121020(20, 10)
323020(20, 30)

Nested loops, one per dimension.

Why: The loop variables r and c are the row and column indexes of the cells.

y depends on the row; x on the column.

Why: The variables x and y are the coordinates, respectively.

Note the crossing.

Why: Row index r gives the y coordinate; column index c gives x — because rows run down the screen and columns run across.

Read the book's example.

Why: If size is 10 pixels, the cell at index (1, 2) would be at coordinates (10, 20) on the screen.

Verify: Check that cell (1, 2) has x = 2 × 10 = 20 and y = 1 × 10 = 10.

Why: The book's sentence says coordinates (10, 20) — reading the pair as (y, x) in grid terms, or with x and y named in the order the loop computes them. Either way, r maps to y and c maps to x, and mixing them up puts your grid on its side.

29. What is array[0] for a 2D array?

Prediction

A 2D array is an array of arrays.

Cell[][] array = new Cell[4][3];
// what is array[0]?
expressiontype
arrayCell[][]
array[0]?
array[0][0]Cell

Predict first

What is array[0]?

  • A Cell[] — the first row, itself an array of three Cells
  • A single Cell
  • An int, the number of columns
  • null

Correct: A Cell[] — the first row, itself an array of three Cells

Why: Each index peels off one dimension: array is an array of rows, array[0] is a row, and array[0][0] is a Cell. That is why array[0].length gives the number of columns, and why the enhanced for loop over array has type Cell[] row.

30. The two lengths

Concept

Because a 2D array is an array of arrays, its two dimensions are reached differently — and the asymmetry is not arbitrary.

public int numRows() {
    return array.length;        // how many rows
}

public int numCols() {
    return array[0].length;     // how long the first row is
}
expressionmeans
array.lengththe number of rows
array[0].lengththe number of columns
array[3].lengthalso the number of columns — if rows are equal length

numRows simply returns the length of the rows array. numCols returns the length of the first row, which is the number of columns. Since the rows all have the same length, we have to check only one. That last clause is an assumption — Java permits ragged arrays where rows differ in length.

31. Swapping the indexes

Trap

The trap

array[c][r] reads a different cell — or crashes.

Cell[][] array = new Cell[5][10];   // 5 rows, 10 columns

array[7][2]      // ArrayIndexOutOfBoundsException - only 5 rows
array[2][7]      // fine
array[c][r]      // a bug that sometimes works
accessrowcolumnvalid for 5×10?
array[2][7]27yes
array[7][2]72no — only 5 rows
on a square grid——no error, wrong cell

The last row is the dangerous case. On a square grid, swapped indexes never throw — the program runs and produces a transposed picture, which is much harder to notice than a crash.

The fix

Row first, always.

for (int r = 0; r < numRows(); r++) {
    for (int c = 0; c < numCols(); c++) {
        Cell cell = array[r][c];
    }
}
conventionkeeps
outer loop over rowsthe traversal in row-major order
r before c, everywhereone rule to remember
numRows and numColsthe lengths named, not counted

Name the loop variables r and c rather than i and j. With i and j there is nothing to check against; with r and c, array[c][r] looks wrong on the page — which is the cheapest possible defence against this bug.

32. How many rows and columns?

Prediction

Row-major order.

Cell[][] array = new Cell[5][10];
expressionvalue
array.length?
array[0].length?

Predict first

What are the two lengths?

  • 5 rows and 10 columns
  • 10 rows and 5 columns
  • 50 rows and 1 column
  • Both are 50

Correct: 5 rows and 10 columns

Why: The first bracket is the number of rows and the second the number of columns, which follows directly from a 2D array being an array of rows. array.length counts the rows; array[0].length measures one of them.

33. Fill the grid

Fill the middle

Rows give y; columns give x.

Fill in the blanks

for (int r = 0; r < rows; r++) r} * size;
for (int c = 0; c < cols; c++) c} * size;
array[r][c] = new Cell(x, y, size);
}
}

Why: Rows stack vertically, so the row index scales the y coordinate; columns run across, so the column index scales x. Note that y is computed in the outer loop — it only changes once per row, so recomputing it inside the inner loop would be wasted work.

34. Must every row be the same length?

Edge cases

numCols checks only array[0].

Discussion prompt

Since the rows all have the same length, we have to check only one. Is Java's 2D array actually required to be rectangular? What does that say about numCols?

Hint: What is new Cell[4][]?

Answer:

No. Because it is an array of arrays, each row is a separate object and can have its own length — a ragged array. new Cell[4][] creates four null row references you can fill with any lengths.

So numCols is making an assumption, not a computation. It happens to hold because the constructor built every row the same length.

A method whose correctness rests on how the constructor happened to build things is worth noticing. Here it is fine and documented; in general, an assumption the compiler cannot check belongs in a comment — the same point as binary search's sorted array in Lesson 12b.

35. The GridCanvas class

Section

Sections 15.4-15.5

36. Is a Canvas, has a grid

Concept

Now that we have a Cell class and a way to represent a 2D array of cells, we can write a class to represent a grid of cells. Chapter 14's vocabulary describes it in one sentence.

public class GridCanvas extends Canvas {
    private Cell[][] array;

    public GridCanvas(int rows, int cols, int size) {
        array = new Cell[rows][cols];
        for (int r = 0; r < rows; r++) {
            int y = r * size;
            for (int c = 0; c < cols; c++) {
                int x = c * size;
                array[r][c] = new Cell(x, y, size);
            }
        }
        // set the canvas size
        setSize(cols * size, rows * size);
    }
}
relationshipwritten asbuys
GridCanvas IS-A Canvasextends Canvasthe drawing machinery
GridCanvas HAS-A Cell[][]an instance variablethe grid

Using vocabulary from the previous chapter, GridCanvas is a Canvas that has a 2D array of cells. By extending the Canvas class from java.awt, we inherit methods for drawing graphics on the screen. Both relationships from Lesson 14b, in one class declaration.

37. The three classes

Picture it

Conway has a GridCanvas, which is a Canvas and has Cells. Every arrow is one of the two relationships.

Figure (svg): A UML diagram showing Conway containing GridCanvas which extends Canvas and contains Cells

GridCanvas knows how to draw a grid; Conway knows the rules. That split is why the next lesson can add update to Conway without touching the drawing code at all.

38. Drawing the whole grid

Worked example

In fact, the code is surprisingly straightforward: to draw the grid, we simply draw each cell.

public void draw(Graphics g) {
    for (Cell[] row : array) {
        for (Cell cell : row) {
            cell.draw(g);
        }
    }
}

public void paint(Graphics g) {
    draw(g);
}
loopvariabletype
outerrowCell[] — one row of the array
innercellCell — one element of that row
bodycell.draw(g)each cell draws itself

Two enhanced for loops, nested.

Why: We use nested for loops to traverse the 2D array.

The outer one gives rows.

Why: The outer loop traverses the rows; the inner loop traverses the cells in each row.

Read it aloud.

Why: For each row in the array, and for each cell in the row, draw the cell in the graphics context.

Each cell draws itself.

Why: Each cell contains its coordinates and size, so it knows how to draw itself.

Verify: Notice that draw never mentions x, y, size or a colour — those all live in Cell.

Why: The enhanced for loop works here because the method needs no indexes. Lesson 15b's countNeighbors will need r and c, and so will use standard loops — which is the clearest example in the book of when each kind of loop is right.

39. What type is row?

Prediction

The outer enhanced for loop over a 2D array.

for (Cell[] row : array) {
    for (Cell cell : row) {
        cell.draw(g);
    }
}
array isso each element is
Cell[][]?

Predict first

Why is the outer variable declared Cell[] rather than Cell?

  • Because the elements of a 2D array are rows, which are themselves arrays
  • Because Java requires arrays in enhanced for loops
  • Because rows are drawn all at once
  • It is a mistake; Cell would also work

Correct: Because the elements of a 2D array are rows, which are themselves arrays

Why: A 2D array is an array of arrays, so iterating over it yields rows, each of type Cell[]. The inner loop then iterates over one row to yield individual Cells — which is the array-of-arrays fact made visible in the loop's own syntax.

40. paint, repaint, and draw

Concept

Classes that extend Canvas are supposed to provide a method called paint that paints the contents of the Canvas.

methodwho calls itwhat it does
paint(Graphics)the window systemcalls draw
draw(Graphics)paint, and youdraws every cell
repaint()your codeasks the system to call paint

It gets invoked when the Canvas is created and anytime it needs to be redrawn; for example, when its window is moved or resized. You never call paint yourself — the reason we use repaint is that it does not require a Graphics object as a parameter, and only the window system has one to give.

41. Calling paint yourself

Trap

The trap

paint needs a Graphics object you do not have.

private void mainloop() {
    while (true) {
        update();
        paint(???);          // where would the Graphics come from?
    }
}
problemdetail
paint takes a Graphicsthe window system supplies it
your code has noneyou cannot make a valid one
and painting is the system's jobit decides when the screen is ready

The graphics context represents the actual drawing surface at a particular moment. Manufacturing one yourself means drawing to something the window system is not showing.

The fix

Call repaint, and let the system call paint.

private void mainloop() {
    while (true) {
        update();
        grid.repaint();      // asks the system to redraw
        ...
    }
}
you callthe system callswhich calls
repaint()paint(g)draw(g)

repaint comes from the Canvas class. By default, it calls the paint method we provided, which calls draw. This is inversion at work: you supply paint and the framework decides when to run it — the first time in this book that library code calls yours rather than the other way round.

42. Which loop for which method?

Definition probe

Enhanced when you do not need indexes; standard when you do.

Sort into buckets

Sort each task.

enhanced for
draw every cell; count how many cells are on
standard for
store a neighbour count at counts[r][c]; look at a cell's neighbours at (r±1, c±1)
enh
The method visits every element and needs no positions, so the enhanced loop is shorter and cannot go out of bounds.
std
The method needs r and c — to store a result at that position, or to look at neighbouring positions — which the enhanced loop does not give you.

43. Grid dimensions

Fill the middle

An array of rows.

Fill in the blanks

public int numRows() length};
}

public int numCols() 0}].length;
}

Why: The array's own length is the number of rows; the length of any one row is the number of columns, and row 0 is as good as any since the constructor made them equal. Note length is a field, not a method — no parentheses, unlike size() on an ArrayList.

44. Why does GridCanvas extend Canvas?

Explain it to yourself

It could have held a Canvas instead.

Discussion prompt

GridCanvas extends Canvas rather than having one as a field. Apply the sentence test, and say what the choice buys.

Hint: What has to be true for the window system to draw it?

Answer:

A GridCanvas is a Canvas — it is a thing that gets drawn on the screen, and the sentence is plainly true.

And it buys substitution: frame.add(game.grid) works because a JFrame accepts a Canvas, and a GridCanvas is one. Composition would require unwrapping it at every such call.

It also lets GridCanvas override paint, which is how the window system reaches your drawing code at all. That is inheritance being used for its second purpose: not just reuse, but a hook the framework calls.

45. Starting the game

Section

Section 15.6

46. The Conway class

Concept

Now we're ready to implement the game. To encapsulate the rules of GoL, we define a class named Conway. The Conway class has a GridCanvas that represents the state of the game.

public class Conway {
    private GridCanvas grid;

    public Conway() {
        grid = new GridCanvas(5, 10, 20);
        grid.turnOn(2, 1);
        grid.turnOn(2, 2);
        grid.turnOn(2, 3);
        grid.turnOn(1, 7);
        grid.turnOn(2, 7);
        grid.turnOn(3, 7);
    }
}
callturns on cell
grid.turnOn(2, 1)row 2, column 1
grid.turnOn(2, 2)row 2, column 2
grid.turnOn(2, 3)row 2, column 3 — a horizontal blinker
grid.turnOn(1, 7)row 1, column 7
grid.turnOn(2, 7)row 2, column 7
grid.turnOn(3, 7)row 3, column 7 — a vertical blinker

This constructor makes a GridCanvas with 5 rows and 10 columns, with cells that are 20 pixels wide and high. It then sets up the initial conditions. Two blinkers, out of phase — so the display is never still.

47. The initial configuration

Picture it

Six calls to turnOn, and this is the grid they produce.

Figure (svg): A five by ten grid showing two blinker patterns, one horizontal and one vertical

Both are blinkers, so after one time step the left becomes vertical and the right becomes horizontal — and they swap back and forth forever. Being out of phase makes the animation obvious.

48. main, and the window

Worked example

Before we implement the rest of the game, we'll write a main method that creates a Conway object and displays it.

public static void main(String[] args) {
    String title = "Conway's Game of Life";
    Conway game = new Conway();
    JFrame frame = new JFrame(title);
    frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    frame.setResizable(false);
    frame.add(game.grid);
    frame.pack();
    frame.setVisible(true);
    game.mainloop();
}
linedoes
new Conway()builds the grid and the initial pattern
new JFrame(title)creates a window on the screen
setDefaultCloseOperationexit the program when the window closes
setResizable(false)resizing is disabled
frame.add(game.grid)put the canvas in the window
frame.pack()resize the frame to fit the canvas
frame.setVisible(true)show it
game.mainloop()start the simulation — Lesson 15b

Build the game, then the window.

Why: After constructing the game object, main constructs a JFrame, which creates a window on the screen.

Configure it.

Why: The JFrame is configured to exit the program when closed. Resizing the window is disabled.

Add, pack, show.

Why: main then adds the GridCanvas inside the frame, resizes (packs) the frame to fit the canvas, and makes the frame visible.

Then run.

Why: game.mainloop() — which does not exist yet.

Verify: Run it and confirm a window appears showing the two blinkers, frozen.

Why: We can use this method to test Cell and GridCanvas, and to develop the other methods we need. That is Lesson 4b's incremental development: get something on the screen before writing the simulation, so that when the simulation misbehaves you can see it.

49. How big is the window?

Prediction

GridCanvas sets its own size.

grid = new GridCanvas(5, 10, 20);
// inside the constructor:
setSize(cols * size, rows * size);
cellssizepixels
width10 columns20?
height5 rows20?

Predict first

What are the canvas dimensions in pixels?

  • 200 wide by 100 tall
  • 100 wide by 200 tall
  • 50 wide by 100 tall
  • 1000 by 1000

Correct: 200 wide by 100 tall

Why: Width comes from the columns (10 × 20 = 200) and height from the rows (5 × 20 = 100) — the same crossing as x = c * size and y = r * size. Note the argument order: setSize takes width first, so cols comes first even though rows come first in the array.

50. Why JFrame rather than Frame

Concept

The book uses a Swing class inside an otherwise AWT program, and its own footnote says why.

classpackagedefault close behaviour
Framejava.awtnone — you must write a handler
JFramejavax.swingsetDefaultCloseOperation does it
Canvasjava.awt—

We are using JFrame (in javax.swing) instead of Frame (in java.awt) for simplicity. Frame does not provide a default close operation; it requires you to implement a method to be called when the user closes the window. A small convenience, chosen so the chapter can stay on 2D arrays rather than on window events — which are Lesson 17c's subject.

51. Reaching into another object's field from main

Trap

The trap

game.grid is private — except that main is inside Conway.

frame.add(game.grid);       // grid is private!

// legal ONLY because main is a static method
// inside the Conway class itself
where main iscan it read game.grid?
inside Conwayyes — private is per class
in another classno

This is the same rule as Lesson 13b's static merge reading d1.cards: private means within this class, not within this object. Move main to another class and the line stops compiling.

The fix

Know why it is legal, and add a getter if main moves.

// inside Conway - fine as written
frame.add(game.grid);

// if main lived elsewhere:
public GridCanvas getGrid() {
    return grid;
}
frame.add(game.getGrid());
approachwhen
direct field accessmain is inside the same class
a gettermain is anywhere else

Code that works because of where it lives is worth a moment's attention. It is not wrong here — it is a common idiom for a class with its own main — but it is a dependency on a fact that a later refactor can quietly break.

52. What does the initial grid show?

Prediction

Six turnOn calls.

grid.turnOn(2, 1);
grid.turnOn(2, 2);
grid.turnOn(2, 3);
rcposition
21row 2
22row 2
23row 2

Predict first

What shape do these three make?

  • Three cells in a horizontal row — a blinker
  • Three cells in a vertical column
  • A diagonal line
  • An L shape

Correct: Three cells in a horizontal row — a blinker

Why: All three share row 2 and differ in column, so they lie side by side horizontally. That is the blinker from Section 15.1: after one time step it becomes vertical, and after two it is back — which is what makes it a good thing to watch while testing.

53. Match the method to who calls it

Matching

Some methods you call; some call you.

Match the pairs

  • a. paint(Graphics g)
  • b. repaint()
  • c. draw(Graphics g)
  • d. turnOn(r, c)
  • r1. the window system calls it
  • r2. you call it to request a redraw
  • r3. paint calls it
  • r4. the Conway constructor calls it

Why: paint is the one you write but never call — the framework invokes it when the window needs drawing. That is inversion of control, and it is why repaint exists: it is how your code asks for something only the system can do.

54. Why put the pattern in the constructor?

Real world

The starting cells are hard-coded.

Discussion prompt

Exercise 15.3 asks you to read a starting pattern from a .cells file instead. What does hard-coding it in the constructor cost, and why start that way anyway?

Hint: What do you have to do to try a different pattern?

Answer:

Every new pattern means editing and recompiling. Trying the Glider or the R-pentomino is a code change rather than a file change.

But it is the right first version: it has no file handling, no parsing, and no error cases, so when the simulation misbehaves you know the problem is the simulation.

Get it working, then make it flexible. Lesson 4b's incremental development and Lesson 5b's generalisation, in that order — the exercise is the second step, and it is much easier once the first is known to work.

55. One-dimensional against two-dimensional

Comparison

Fill the blanks.

Comparison matrix

int[] aint[][] a
created withnew int[5]new int[4][3]
a[0] isan intan int[] — a whole row
a.length isthe number of elementsthe number of rows
the number of columns—a[0].length
enhanced for giveseach inteach row, as an int[]

Every row follows from one fact: a 2D array is an array of arrays. Once that is fixed in your head, none of the syntax has to be memorised separately.

56. The pattern to carry away

Pattern

Traversing a 2D array, and choosing which kind of loop.

// when you need the indexes - standard loops
for (int r = 0; r < numRows(); r++) {
    for (int c = 0; c < numCols(); c++) {
        counts[r][c] = countAlive(r, c);
    }
}

// when you only need the elements - enhanced loops
for (Cell[] row : array) {
    for (Cell cell : row) {
        cell.draw(g);
    }
}
rulewhy
row first, then columnrow-major order — array[r][c]
r maps to y, c maps to xrows run down, columns run across
array.length is rowsthe outer array holds the rows
array[0].length is columnsa row's own length
name the variables r and cso a swapped index looks wrong

57. Check: dimensions

Check

Work it out before you click.

Cell[][] array = new Cell[5][10];
expressionvalue
array.length?
array[0].length?

Check your understanding

What are these two values?

  • A. 5 and 10 — rows then columns (correct)
  • B. 10 and 5
  • C. 50 and 50
  • D. 5 and 5

Answer: A

Why: The array holds five rows, so its own length is 5; each row is an array of ten Cells, so array[0].length is 10. The asymmetry between the two expressions is not a quirk — it follows directly from a 2D array being an array of arrays in row-major order.

Why B tempts people
That reverses row-major order; the first bracket is always rows.
Why C tempts people
There are 50 cells, but neither length expression counts them all.
Why D tempts people
The rows and columns differ here — 5 by 10, not square.

58. Check: the enhanced loop

Check

Work it out before you click.

for (Cell[] row : array) {
    for (Cell cell : row) {
        cell.draw(g);
    }
}
loopiterates over
outerarray
innerrow

Check your understanding

Why can draw use enhanced for loops when countNeighbors cannot?

  • A. draw needs only the elements; countNeighbors needs r and c to store results at counts[r][c] (correct)
  • B. Enhanced loops do not work on int arrays
  • C. draw is faster
  • D. countNeighbors traverses in a different order

Answer: A

Why: An enhanced for loop gives you each element but never its position. draw asks each cell to draw itself, and the cell already knows its own coordinates — so no index is needed. countNeighbors has to write a result at counts[r][c] and look at neighbouring positions, both of which require the indexes.

Why B tempts people
Enhanced loops work on any array type, including int[].
Why C tempts people
Speed is not the consideration; what information the loop gives you is.
Why D tempts people
Both traverse in row-major order; the difference is what they need to know.

59. Check: relationships

Check

Work it out before you click.

public class GridCanvas extends Canvas {
    private Cell[][] array;
}
relationship
GridCanvas and Canvas?
GridCanvas and Cell?

Check your understanding

What are the two relationships?

  • A. GridCanvas IS-A Canvas and HAS-A 2D array of Cells (correct)
  • B. GridCanvas HAS-A Canvas and IS-A Cell
  • C. Both are IS-A
  • D. Both are HAS-A

Answer: A

Why: The extends clause makes it a Canvas — which is what lets frame.add(grid) work and lets the window system call its paint. The instance variable makes it a container of Cells. Think Java states exactly this: GridCanvas is a Canvas that has a 2D array of cells.

Why B tempts people
This reverses both; there is no Canvas field and no extends Cell.
Why C tempts people
A GridCanvas is not a Cell — it contains many.
Why D tempts people
The extends Canvas clause is inheritance, not composition.

60. Grids are everywhere

Real world

A 2D array of small objects, updated on a clock, is one of the most reused shapes in programming.

Discussion prompt

Where else does this exact structure appear — a grid whose cells change based on their neighbours?

Hint: Screens, spreadsheets, weather.

Answer:

Every image is one: a 2D array of pixels, and most image filters — blur, sharpen, edge detection — compute each output pixel from its eight neighbours. The code shape is identical to countAlive.

Spreadsheets are a grid where each cell's value depends on others, board games are grids of pieces, and physical simulations — heat spreading through a plate, fluid, weather — divide space into cells that exchange with their neighbours.

And they all share the simultaneity problem. Updating a cell before its neighbours have been read corrupts the result, which is why a second array for the new values is the standard answer everywhere, not just here.

61. How sure are you?

Commit first

Commit to an answer and to your confidence.

Predict first

In Java, what is a two-dimensional array really?

  • An array whose elements are themselves arrays — one per row
  • A single flat block of memory with two indexes computed into it
  • A special built-in type distinct from ordinary arrays
  • An ArrayList of ArrayLists

Correct: An array whose elements are themselves arrays — one per row

Why: In Java, a 2D array is really an array of arrays. You can think of it as an array of rows, where each row is an array. That fact explains everything else: why array[0] has type Cell[], why array.length gives rows while array[0].length gives columns, why the enhanced for loop's outer variable is declared Cell[], and why rows can legally have different lengths. Some languages do use a flat block with computed offsets — Java does not, which is exactly why ragged arrays are possible here.

62. Explain it to someone else

Explain it

Two minutes, out loud.

Discussion prompt

A classmate keeps writing array[c][r] and cannot see why their grid comes out sideways. Explain row-major order and give them a habit that prevents it.

Hint: What is array[0]?

Answer:

A 2D array is an array of arrays — an array of rows. So the first index picks a row and the second picks an element within that row. array[0] is a whole row, not a single element.

That is why array.length is the number of rows and array[0].length is the number of columns. Writing array[c][r] asks for row c, which on a non-square grid is out of bounds and on a square grid is silently the wrong cell.

The habit: name the loop variables r and c rather than i and j. Then array[c][r] looks wrong on the page — the names do the checking that the compiler cannot.

63. Exit ticket

Exit ticket

One question before you close the deck.

Predict first

Why must all cells be updated simultaneously in the Game of Life?

  • Because each cell's next state depends on its neighbours' current states, so changing one first corrupts the counts for the rest
  • Because Java arrays cannot be modified during a loop
  • Because it is faster
  • Because the rules are applied in a random order

Correct: Because each cell's next state depends on its neighbours' current states, so changing one first corrupts the counts for the rest

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. If you updated as you went, the second cell would count neighbours in a grid the first had already changed — so the first cell would be right and everything after it contaminated. The program achieves simultaneity by traversing twice: once to fill a separate counts array, once to apply the rules from it. Lesson 15b writes both passes.

64. Draw the whole lesson

Connect it up

One page, from memory.

Draw it

Draw a 4×3 array of arrays: an outer array of four row references, each pointing at a row of three elements, and label array.length and array[0].length. Beside it write the three rules of the Game of Life and trace one time step of a blinker, showing the neighbour count for all five cells involved. Then draw the UML for Conway, GridCanvas, Canvas and Cell with the right arrowheads, and write one sentence saying which class the window system calls into and which method it calls.

65. Recap

Recap

Six sections that build the board before the game.

if you remember one thingit is this
about 2D arraysan array of rows — everything follows
about traversalr before c, and name them r and c
about the frameworkyou write paint; you call repaint

Sources

  1. Downey & Mayfield, Think Java, 2nd edition (Green Tea Press / O'Reilly, 2020) — Think Java 2e, Chapter 15 (Arrays of Arrays), Sections 15.1-15.6, pp. 249-257
  2. The Java Tutorials — Arrays (multidimensional)
  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