Allocate a rectangular Java array:
java
boolean[][] board = new boolean[1000][1000];
Java initializes every new boolean array element to false, so all cells are initially dead without a filling loop. This allocates a top-level array of row references and a distinct boolean array for every row.
Count only the existing neighbours, excluding the center, then apply the Conway Game of Life transition:
java
static boolean nextCell(boolean[][] old, int i, int j) {
    int height = old.length;
    int width = old[0].length;
    int neighbours = 0;
    for (int di = -1; di <= 1; di++) {
        for (int dj = -1; dj <= 1; dj++) {
            if (di == 0 && dj == 0) continue;
            int r = i + di;
            int c = j + dj;
            if (r >= 0 && r < height && c >= 0 && c < width
                    && old[r][c]) {
                neighbours++;
            }
        }
    }
    return neighbours == 3 || (old[i][j] && neighbours == 2);
}
The short-circuit bounds test occurs before indexing a neighbour. Out-of-range cells therefore contribute zero, including at corners, and no opposite-edge wrapping occurs. The center is explicitly excluded. Birth requires three neighbours; survival requires two or three. The method accepts a nonempty rectangular board and a valid cell index and does not change the old Java array.
Use disjoint Java arrays for the old and new generations:
java
static void step(boolean[][] old, boolean[][] next) {
    for (int i = 0; i < old.length; i++) {
        for (int j = 0; j < old[i].length; j++) {
            next[i][j] = nextCell(old, i, j);
        }
    }
}
Every decision reads only old, so all cells undergo the simultaneous Conway Game of Life transition. The second board must have the same dimensions and share no row arrays with the first. Afterwards swap their references for the following step.
Writing each new cell immediately into the input board would expose a mixture of old and new states to later neighbour counts. The result would depend on the traversal order and would no longer be the specified simultaneous automaton. For example, put the three live cells at on an otherwise dead board. The simultaneous result is the vertical triple . During a naive row-major overwrite, becomes live first. The next cell now sees that new cell as well as the old cells , so it is incorrectly born with three counted neighbours. This explicitly exhibits the mixture of generations. A second generation array, or explicit preservation of overwritten old information, is essential.
A growable-array library should store its backing Java array and logical size in private fields in Java. Public methods such as add, get and remove can enforce bounds and keep the size consistent with the stored elements. The representation can later change without requiring clients to modify direct field accesses.
The benefit is preserving invariants through encapsulation. If clients could assign the size or replace the backing Java array directly, they could create negative sizes, report elements that do not exist, or modify entries without the library's checks. Accessor methods should not expose a mutable backing object indiscriminately, since a private field alone does not prevent that indirect route. Private fields also do not provide a general security boundary against privileged reflection or native code; the point here is ordinary library access discipline.
A Java array can hold references to row arrays. For example,
double[][] grid = new double[rows][columns];
grid[2][3] = 4.5;
allocates the outer array and all equal-length rows, with numeric entries initially zero. A ragged alternative is
double[][] grid = new double[rows][];
for (int r = 0; r < rows; r++)
    grid[r] = new double[r + 1];
The second form initially has null row references, so each row must be allocated before access. Rows need not have equal lengths, and a two-dimensional array of objects initially contains null object references rather than constructed cell objects. Java represents a two-dimensional array as an array of arrays.
Use a Spreadsheet class owning a private rectangular Java array of cells. Each immutable internal Cell contains a private tag Kind with values EMPTY, LABEL, NUMBER, FORMULA, and the corresponding payload: a string, a double or a formula reference. The tag determines which payload is meaningful, and the spreadsheet's setters create valid combinations rather than letting callers change fields independently. The cell object need not be public; public methods can expose its kind and label when needed for display.
Provide public setEmpty(r,c), setLabel(r,c,text), setNumber(r,c,x) and setFormula(r,c,f) methods, each checking its target coordinates and invalidating cached results. A public valueAt(r,c) supplies the numerical value, treating out-of-grid, empty and label cells as zero, and a public recalculate() brings all valid formulas up to date. Keep cached numeric results and evaluation-state flags private; they are derived data rather than editable cell content.
Define a public Formula interface with double evaluate(Lookup values, int row, int column). Lookup supplies only valueAt(r,c), so a formula can read dependencies without needing direct access to cells or a mutation API. An immutable AddFormula stores four integer offsets, two row/column pairs. It evaluates the two coordinates relative to the formula's own position and returns their sum. New formula kinds implement the same interface, avoiding changes to the storage representation or a large central switch over all formula operations.
The private spreadsheet dependency evaluation mechanism provides the Lookup implementation, checks cycles and caches evaluated cells. Formula evaluation is required to be side-effect-free with fixed dependencies during a recalculation. This separates encapsulation in object-oriented programming from extension: callers can change a cell through public setters and add new formula implementations, while internal cell invariants and cached values remain protected. Use tagged cell contents, an extensible formula interface and spreadsheet-owned evaluation state.