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
Title
Think Java 2e · Chapter 15 · Arrays of Arrays
Sections 15.1-15.6 · pp. 249-257
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.
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.
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.
Section
Section 15.1
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 is | live neighbours | next state |
|---|---|---|
| alive | 0 or 1 | dead — underpopulation |
| alive | 2 or 3 | alive |
| alive | 4 to 8 | dead — overpopulation |
| dead | exactly 3 | alive — reproduction |
| dead | anything else | dead |
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.
Notation
Notice some consequences of these rules. Each one is a prediction you can check by hand.
Annotate
That turns out to be more interesting than it sounds. Three rules, no randomness, no input — and behaviour nobody can predict without running it.
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
| cell | live neighbours | rule applied | becomes |
|---|---|---|---|
| the centre | 2 | alive with 2 — survives | alive |
| the left cell | 1 | alive with fewer than 2 | dead |
| the right cell | 1 | same | dead |
| directly above centre | 3 | dead with exactly 3 | alive |
| directly below centre | 3 | same | alive |
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.
Prediction
Check every rule.
// dies if fewer than 2
// dies if more than 3
// a dead cell comes alive at exactly 3| rule | applies? |
|---|---|
| fewer than 2 | no — it has 2 |
| more than 3 | no |
Predict first
What happens to it?
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.
Concept
The rules dictate the structure of the code before a line is written.
| the rules require | which means the program needs |
|---|---|
| a grid of cells with two states | a Cell class and a 2D array |
| counting eight neighbours | a way to read a cell at (r, c) |
| cells on the edge have fewer | handling out-of-bounds lookups |
| all cells update simultaneously | count everything before changing anything |
| time steps | a 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.
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
}| cell | counts neighbours in | sees |
|---|---|---|
| (0, 0) | the original grid | correct |
| (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.
Count everything, then update everything.
int[][] counts = countNeighbors(); // pass 1: read only
updateGrid(counts); // pass 2: write only| pass | reads | writes |
|---|---|---|
| countNeighbors | the grid | a separate counts array |
| updateGrid | the counts array | the grid |
The rules of GoL specify that you have to update the cells simultaneously; that is, you have to count the neighbors for all cells before you can update any of them. Two passes, and the counts array is what makes simultaneity possible — Lesson 15b writes both.
Definition probe
Apply the rules to each case.
Sort into buckets
Sort each cell by its next state.
Prediction
One cell, no neighbours.
// a live cell with fewer than two live neighbors dies| cell | live neighbours |
|---|---|
| the only live cell | 0 |
Predict first
What happens?
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.
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.
Section
Section 15.2
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;
}
}| field | final? | why |
|---|---|---|
| x | yes | a cell never moves |
| y | yes | same |
| size | yes | a cell never changes size |
| state | no | it 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.
Notation
A cell is alive or dead. That is exactly two values, and the book still uses an integer.
Annotate
COLORS[state] does — a boolean cannot.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.
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);
}| line | draws |
|---|---|
| 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.
Prediction
The other three fields are.
private final int x;
private final int y;
private final int size;
private int state;| field | changes during the game? |
|---|---|
| x, y, size | no |
| state | yes, every time step |
Predict first
What is the reason?
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.
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;
}| generic | customised | reads as |
|---|---|---|
| getState() == 1 | isOn() | if the cell is on |
| getState() == 0 | isOff() | 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.
Trap
getState and setState spread the encoding everywhere.
if (cell.getState() == 1) { ... }
cell.setState(0);
cell.setState(2); // a state that does not exist| problem | consequence |
|---|---|
| callers compare against 1 | the encoding is copied into every call site |
| setState takes any int | nothing 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.
Methods that can only produce valid states.
public void turnOn() { state = 1; }
public void turnOff() { state = 0; }
public boolean isOn() { return state == 1; }| guarantee | how |
|---|---|
| state is always 0 or 1 | only these methods assign it |
| COLORS[state] is always valid | follows from the above |
| callers never see the encoding | they 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.
Definition probe
Ask whether the value ever changes after construction.
Sort into buckets
Sort each field of Cell.
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.
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.
Section
Section 15.3
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];| expression | is |
|---|---|
| array | an array of 4 rows |
| array[0] | a row — itself an array of 3 Cells |
| array[0][2] | one Cell |
| all elements initially | null |
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.
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.
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);
}
}| r | c | y = r * size | x = c * size | cell at |
|---|---|---|---|---|
| 0 | 0 | 0 | 0 | (0, 0) pixels |
| 0 | 1 | 0 | 10 | (10, 0) |
| 1 | 2 | 10 | 20 | (20, 10) |
| 3 | 2 | 30 | 20 | (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.
Prediction
A 2D array is an array of arrays.
Cell[][] array = new Cell[4][3];
// what is array[0]?| expression | type |
|---|---|
| array | Cell[][] |
| array[0] | ? |
| array[0][0] | Cell |
Predict first
What is array[0]?
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.
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
}| expression | means |
|---|---|
| array.length | the number of rows |
| array[0].length | the number of columns |
| array[3].length | also 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.
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| access | row | column | valid for 5×10? |
|---|---|---|---|
| array[2][7] | 2 | 7 | yes |
| array[7][2] | 7 | 2 | no — 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.
Row first, always.
for (int r = 0; r < numRows(); r++) {
for (int c = 0; c < numCols(); c++) {
Cell cell = array[r][c];
}
}| convention | keeps |
|---|---|
| outer loop over rows | the traversal in row-major order |
| r before c, everywhere | one rule to remember |
| numRows and numCols | the 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.
Prediction
Row-major order.
Cell[][] array = new Cell[5][10];| expression | value |
|---|---|
| array.length | ? |
| array[0].length | ? |
Predict first
What are the two lengths?
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.
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.
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.
Section
Sections 15.4-15.5
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);
}
}| relationship | written as | buys |
|---|---|---|
| GridCanvas IS-A Canvas | extends Canvas | the drawing machinery |
| GridCanvas HAS-A Cell[][] | an instance variable | the 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.
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.
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);
}| loop | variable | type |
|---|---|---|
| outer | row | Cell[] — one row of the array |
| inner | cell | Cell — one element of that row |
| body | cell.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.
Prediction
The outer enhanced for loop over a 2D array.
for (Cell[] row : array) {
for (Cell cell : row) {
cell.draw(g);
}
}| array is | so each element is |
|---|---|
| Cell[][] | ? |
Predict first
Why is the outer variable declared Cell[] rather than Cell?
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.
Concept
Classes that extend Canvas are supposed to provide a method called paint that paints the contents of the Canvas.
| method | who calls it | what it does |
|---|---|---|
| paint(Graphics) | the window system | calls draw |
| draw(Graphics) | paint, and you | draws every cell |
| repaint() | your code | asks 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.
Trap
paint needs a Graphics object you do not have.
private void mainloop() {
while (true) {
update();
paint(???); // where would the Graphics come from?
}
}| problem | detail |
|---|---|
| paint takes a Graphics | the window system supplies it |
| your code has none | you cannot make a valid one |
| and painting is the system's job | it 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.
Call repaint, and let the system call paint.
private void mainloop() {
while (true) {
update();
grid.repaint(); // asks the system to redraw
...
}
}| you call | the system calls | which 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.
Definition probe
Enhanced when you do not need indexes; standard when you do.
Sort into buckets
Sort each task.
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.
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.
Section
Section 15.6
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);
}
}| call | turns 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.
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.
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();
}| line | does |
|---|---|
| new Conway() | builds the grid and the initial pattern |
| new JFrame(title) | creates a window on the screen |
| setDefaultCloseOperation | exit 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.
Prediction
GridCanvas sets its own size.
grid = new GridCanvas(5, 10, 20);
// inside the constructor:
setSize(cols * size, rows * size);| cells | size | pixels | |
|---|---|---|---|
| width | 10 columns | 20 | ? |
| height | 5 rows | 20 | ? |
Predict first
What are the canvas dimensions in pixels?
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.
Concept
The book uses a Swing class inside an otherwise AWT program, and its own footnote says why.
| class | package | default close behaviour |
|---|---|---|
| Frame | java.awt | none — you must write a handler |
| JFrame | javax.swing | setDefaultCloseOperation does it |
| Canvas | java.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.
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 is | can it read game.grid? |
|---|---|
| inside Conway | yes — private is per class |
| in another class | no |
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.
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());| approach | when |
|---|---|
| direct field access | main is inside the same class |
| a getter | main 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.
Prediction
Six turnOn calls.
grid.turnOn(2, 1);
grid.turnOn(2, 2);
grid.turnOn(2, 3);| r | c | position |
|---|---|---|
| 2 | 1 | row 2 |
| 2 | 2 | row 2 |
| 2 | 3 | row 2 |
Predict first
What shape do these three make?
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.
Matching
Some methods you call; some call you.
Match the pairs
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.
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.
Comparison
Fill the blanks.
Comparison matrix
| int[] a | int[][] a | |
|---|---|---|
| created with | new int[5] | new int[4][3] |
| a[0] is | an int | an int[] — a whole row |
| a.length is | the number of elements | the number of rows |
| the number of columns | — | a[0].length |
| enhanced for gives | each int | each 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.
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);
}
}| rule | why |
|---|---|
| row first, then column | row-major order — array[r][c] |
| r maps to y, c maps to x | rows run down, columns run across |
| array.length is rows | the outer array holds the rows |
| array[0].length is columns | a row's own length |
| name the variables r and c | so a swapped index looks wrong |
Check
Work it out before you click.
Cell[][] array = new Cell[5][10];| expression | value |
|---|---|
| array.length | ? |
| array[0].length | ? |
Check your understanding
What are these two values?
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.
Check
Work it out before you click.
for (Cell[] row : array) {
for (Cell cell : row) {
cell.draw(g);
}
}| loop | iterates over |
|---|---|
| outer | array |
| inner | row |
Check your understanding
Why can draw use enhanced for loops when countNeighbors cannot?
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.
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?
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.
extends Cell.extends Canvas clause is inheritance, not composition.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.
Commit first
Commit to an answer and to your confidence.
Predict first
In Java, what is a two-dimensional array really?
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.
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.
Exit ticket
One question before you close the deck.
Predict first
Why must all cells be updated simultaneously in the Game of Life?
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.
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.
Recap
Six sections that build the board before the game.
| if you remember one thing | it is this |
|---|---|
| about 2D arrays | an array of rows — everything follows |
| about traversal | r before c, and name them r and c |
| about the framework | you write paint; you call repaint |
Want this taught 1-on-1? Alexander tutors Java — $55/session, free consultation.