AP Computer Science A — Mastering 2D Arrays (Data Collections)

2D Arrays

A 2D array in Java is a way to store values in a grid-like structure: rows and columns. Conceptually, you can think of it like a spreadsheet, a seating chart, a game board, or an image made of pixels. Each position in the grid holds one element (like an int, double, String, or even an object).

What a 2D array really is in Java

In Java, a 2D array is implemented as an array of arrays. That detail matters because it explains several behaviors you’ll see when you code:

  • The “outer” array holds references to “inner” arrays (each inner array is a row).
  • Rows can technically have different lengths (called a ragged or jagged array), even though many problems use rectangular arrays where every row has the same number of columns.

So when you write int[][] grid, you should mentally read it as: “grid is an array where each element is an int[] row.”

Why 2D arrays matter

2D arrays show up whenever data is naturally organized in two dimensions:

  • Board games (chess, tic-tac-toe, Battleship)
  • Images (pixel intensities or RGB values)
  • Maps (terrain heights, occupancy grids)
  • Tables (scores by player and round)

On the AP exam, 2D arrays are a common setting for algorithmic thinking: you’re asked to traverse, compute totals, search for patterns, or update cells based on rules.

Declaring and creating 2D arrays

A 2D array variable declaration specifies the element type and two bracket pairs:

int[][] grid;
String[][] names;

To actually allocate memory, you use new.

Rectangular allocation

This creates a grid with a fixed number of rows and columns:

int[][] grid = new int[3][4]; // 3 rows, 4 columns each

All numeric values default to 0, boolean defaults to false, and object references default to null.

A very common misconception is thinking new int[3][4] gives you indices 1..3 and 1..4. Java arrays are 0-indexed, so rows are 0..2 and columns are 0..3.

Initializer lists

You can also create and fill a 2D array with literals:

int[][] grid = {
    {1, 2, 3},
    {4, 5, 6}
};

Here, grid.length is 2 (two rows), and each row has length 3.

Ragged arrays (jagged rows)

Because a 2D array is an array of arrays, you can allocate rows separately:

int[][] ragged = new int[3][];  // 3 rows, columns not set yet
ragged[0] = new int[2];
ragged[1] = new int[5];
ragged[2] = new int[1];

AP questions often use rectangular arrays, but you must still write code that doesn’t accidentally assume every row has the same length unless the problem states it.

Accessing and updating elements

To access a cell, you use two indices: array[row][col].

int value = grid[1][2];    // row 1, column 2
grid[0][0] = 99;           // update top-left cell

A useful mental model is “row first, then column.” If you picture coordinates, it’s like (y, x) rather than (x, y).

Finding the number of rows and columns

Two length expressions are essential:

  • grid.length is the number of rows (outer array length)
  • grid[r].length is the number of columns in row r (inner array length)

Example:

int rows = grid.length;
int colsInFirstRow = grid[0].length;

Common error: using grid.length for both dimensions. That only works for square arrays (and even then it’s bad practice).

Example: building a simple seating chart

Suppose you want 5 rows of seats and 6 seats per row:

String[][] seats = new String[5][6];
seats[0][0] = "Ava";
seats[0][1] = "Noah";

If a seat is still null, it means nobody has been assigned there yet.

Exam Focus
  • Typical question patterns:
    • Given a type[][], determine what arr.length and arr[0].length represent and use them correctly.
    • Read or write an element at a specific row/column and describe what changes.
    • Identify whether code risks an ArrayIndexOutOfBoundsException.
  • Common mistakes:
    • Swapping indices (using [col][row] instead of [row][col]). A good habit is naming variables r and c.
    • Assuming all rows have the same length when the problem doesn’t guarantee it.
    • Using 1-based counting when arrays are 0-based.

Traversing 2D Arrays

To traverse a 2D array means to systematically visit elements—usually using nested loops. Traversal is the foundation of almost every 2D array algorithm: you can’t sum, search, count, or modify values without a reliable way to move through the grid.

The core idea: nested loops

Because there are two dimensions, you typically need two loops:

  • An outer loop chooses a row.
  • An inner loop walks across columns within that row.

This is called row-major order traversal (row by row).

for (int r = 0; r < grid.length; r++) {
    for (int c = 0; c < grid[r].length; c++) {
        System.out.print(grid[r][c] + " ");
    }
    System.out.println();
}

Notice the inner loop uses grid[r].length, not grid[0].length. That works for both rectangular and ragged arrays.

Why traversal order matters

Sometimes order doesn’t matter (like computing a total sum). But for many problems it does:

  • If you are computing something based on neighbors (up/down/left/right), order affects whether you see “old” or “already-updated” values.
  • If you’re printing the grid, row-major order matches how humans usually read tables.
  • Some problems specifically ask for column-major order.

Column-major traversal

In column-major order, you loop columns first, then rows.

This is only safe if every row has at least that many columns (so it’s most appropriate for rectangular arrays).

for (int c = 0; c < grid[0].length; c++) {
    for (int r = 0; r < grid.length; r++) {
        System.out.print(grid[r][c] + " ");
    }
    System.out.println();
}

If the array could be ragged, column-major traversal needs extra checks (or a different strategy) because grid[r].length might be smaller for some rows.

Enhanced for loops with 2D arrays

Java’s enhanced for loop (for-each) can traverse rows and elements cleanly:

for (int[] row : grid) {
    for (int value : row) {
        System.out.print(value + " ");
    }
    System.out.println();
}

This is great for read-only traversals (like summing or printing). But it has limitations:

  • You don’t directly have the indices r and c, which many problems require.
  • If you try to assign to the loop variable (value = 10;), it does not change the array element because value is a copy of the element, not a reference to the cell.

If you need to update elements, use indexed loops:

for (int r = 0; r < grid.length; r++) {
    for (int c = 0; c < grid[r].length; c++) {
        grid[r][c] *= 2;
    }
}

Traversing partial regions

AP-style questions often focus on a sub-rectangle or region, such as “only the border,” “only row 2,” or “only the upper-left 3x3.” The key skill is to adjust loop bounds carefully.

Example: traverse only a single row
int r = 2;
for (int c = 0; c < grid[r].length; c++) {
    System.out.println(grid[r][c]);
}
Example: traverse only a single column (rectangular assumption)
int c = 1;
for (int r = 0; r < grid.length; r++) {
    System.out.println(grid[r][c]);
}

If the array might be ragged, you must check c < grid[r].length before accessing grid[r][c].

Boundary awareness and index errors

Most traversal bugs are off-by-one errors or incorrect bounds:

  • Using <= instead of < causes you to access one past the last valid index.
  • Mixing up row count and column count causes incorrect bounds.
  • Assuming non-empty arrays: grid[0] is invalid if grid.length == 0.

On the AP exam, arrays are usually non-empty unless stated otherwise, but you should still think about what the code assumes.

Worked example: compute the sum of all elements

To sum everything, you visit every cell once and add it to an accumulator.

public static int sumAll(int[][] grid) {
    int sum = 0;
    for (int r = 0; r < grid.length; r++) {
        for (int c = 0; c < grid[r].length; c++) {
            sum += grid[r][c];
        }
    }
    return sum;
}

What can go wrong?

  • If you accidentally use grid.length for the inner loop, you’ll either miss elements (if there are more columns than rows) or throw an exception (if there are fewer columns than rows).

Memory aid: “RC then access”

A simple habit that prevents many mistakes: decide Rows first, then Columns, then access grid[r][c]. Say it in your head: “row, column.”

Exam Focus
  • Typical question patterns:
    • Write nested loops to traverse a 2D array in row-major order.
    • Determine the output of code that prints or modifies a 2D array using nested loops.
    • Compare row-major vs column-major traversal and pick the correct one for a task.
  • Common mistakes:
    • Using grid[0].length everywhere (breaks ragged arrays and can even break rectangular arrays if grid is empty).
    • Trying to modify elements using an enhanced for loop variable.
    • Off-by-one errors: using <= grid.length - 1 correctly is equivalent to < grid.length, but students often mix forms and slip into <= grid.length.

Implementing 2D Array Algorithms

A 2D array algorithm is a step-by-step procedure that uses traversal to compute information from the grid (like totals or counts) or to transform it (like replacing values or applying rules). On AP Computer Science A, you’re not expected to invent advanced algorithms—you’re expected to write correct, readable nested-loop logic that matches a specification.

A process that helps you write correct algorithms

When you’re given a 2D array problem, especially an FRQ-style prompt, a reliable approach is:

  1. Clarify the goal: Are you computing a result (sum, count, max) or modifying the array?
  2. Pick traversal pattern: row-major is most common; choose column-major only if needed.
  3. Choose variables:
    • accumulator (sum, count, max)
    • loop indices (r, c)
  4. Handle boundaries: especially for neighbor-based rules and ragged rows.
  5. Test mentally on a tiny grid (like 2x3) to confirm indices.

Aggregation algorithms (sum, average, count)

These algorithms “reduce” the grid to a single value.

Example: count values meeting a condition

Count how many entries are negative:

public static int countNegatives(int[][] grid) {
    int count = 0;
    for (int r = 0; r < grid.length; r++) {
        for (int c = 0; c < grid[r].length; c++) {
            if (grid[r][c] < 0) {
                count++;
            }
        }
    }
    return count;
}

This pattern (initialize accumulator, traverse, update accumulator inside an if) is extremely common.

Example: compute average (watch integer division)

To compute an average, you need both a sum and a count of elements.

public static double average(int[][] grid) {
    int sum = 0;
    int count = 0;

    for (int r = 0; r < grid.length; r++) {
        for (int c = 0; c < grid[r].length; c++) {
            sum += grid[r][c];
            count++;
        }
    }

    return (double) sum / count;
}

A classic mistake is returning sum / count without casting; with int arithmetic, you’d lose the fractional part.

Finding extremes (min/max) and their locations

Sometimes you need the largest value, or the location of the largest value.

Example: find maximum value and its position
public static int[] maxLocation(int[][] grid) {
    int maxR = 0;
    int maxC = 0;
    int max = grid[0][0];

    for (int r = 0; r < grid.length; r++) {
        for (int c = 0; c < grid[r].length; c++) {
            if (grid[r][c] > max) {
                max = grid[r][c];
                maxR = r;
                maxC = c;
            }
        }
    }

    return new int[] { maxR, maxC };
}

Key idea: you need both the best-so-far value (max) and where it occurred (maxR, maxC).

Common pitfall: initializing max to 0 will fail if all values are negative. Using grid[0][0] avoids that (assuming a non-empty array, which AP problems typically ensure).

Row and column computations

A very AP-friendly skill is computing things per row or per column, such as row totals, column averages, or checking whether any row meets a criterion.

Example: row sums
public static int[] rowSums(int[][] grid) {
    int[] sums = new int[grid.length];

    for (int r = 0; r < grid.length; r++) {
        int sum = 0;
        for (int c = 0; c < grid[r].length; c++) {
            sum += grid[r][c];
        }
        sums[r] = sum;
    }

    return sums;
}

Notice how the accumulator sum resets for each row.

Example: column sums (rectangular)

If you know the grid is rectangular, you can treat every row as having the same number of columns:

public static int[] colSumsRectangular(int[][] grid) {
    int cols = grid[0].length;
    int[] sums = new int[cols];

    for (int c = 0; c < cols; c++) {
        int sum = 0;
        for (int r = 0; r < grid.length; r++) {
            sum += grid[r][c];
        }
        sums[c] = sum;
    }

    return sums;
}

If the array might be ragged, column sums are more complicated because some rows might not have a given column.

Search algorithms in 2D arrays

Searching in a 2D array is usually a full traversal unless the problem guarantees special ordering.

Example: check if a target exists
public static boolean contains(int[][] grid, int target) {
    for (int r = 0; r < grid.length; r++) {
        for (int c = 0; c < grid[r].length; c++) {
            if (grid[r][c] == target) {
                return true;
            }
        }
    }
    return false;
}

This uses an early return to stop as soon as the target is found.

Common mistake: forgetting the return false at the end, or placing it too early (inside the loops), which would stop after checking only part of the grid.

Update and transformation algorithms

Many 2D problems ask you to modify the array: replace certain values, apply a rule, or build a new grid based on an old one.

Example: replace all zeros with -1
public static void replaceZeros(int[][] grid) {
    for (int r = 0; r < grid.length; r++) {
        for (int c = 0; c < grid[r].length; c++) {
            if (grid[r][c] == 0) {
                grid[r][c] = -1;
            }
        }
    }
}

This modifies the original array in place.

A subtle misconception: students sometimes think a method like this needs to return the array. In Java, arrays are objects; when you pass the array into a method, the method can mutate its elements.

Neighbor-based algorithms (grids with rules)

A classic grid scenario: each cell updates based on adjacent cells (up, down, left, right, or diagonals). These problems are common because they test careful boundary checks.

The boundary problem

If you are at the top row (r == 0), there is no “up” neighbor (r - 1). Similarly:

  • bottom row: r == grid.length - 1
  • left edge: c == 0
  • right edge: c == grid[r].length - 1

A safe pattern is: before accessing grid[nr][nc], ensure indices are in range.

Example: count “high” cells compared to the one above (rectangular)

Count how many cells are strictly greater than the cell directly above them. Start from row 1 since row 0 has no above neighbor.

public static int countHigherThanAbove(int[][] grid) {
    int count = 0;

    for (int r = 1; r < grid.length; r++) {
        for (int c = 0; c < grid[r].length; c++) {
            if (grid[r][c] > grid[r - 1][c]) {
                count++;
            }
        }
    }

    return count;
}

This assumes the grid is rectangular (or at least that row r-1 has a column c whenever row r does). If raggedness is possible, you would need c < grid[r - 1].length as an additional condition.

Building a new 2D array from an old one (copy/transform)

Sometimes you should not change the original grid; instead, you create a new one.

Example: create a “threshold” grid

Return a boolean grid where each cell is true if the corresponding value is at least threshold.

public static boolean[][] atLeast(int[][] grid, int threshold) {
    boolean[][] result = new boolean[grid.length][];

    for (int r = 0; r < grid.length; r++) {
        result[r] = new boolean[grid[r].length];
        for (int c = 0; c < grid[r].length; c++) {
            result[r][c] = grid[r][c] >= threshold;
        }
    }

    return result;
}

Two important ideas are happening here:

  • We allocate result with the same number of rows as grid.
  • We allocate each row result[r] to match grid[r].length, which supports ragged arrays.

A common bug is writing new boolean[grid.length][grid[0].length] without considering raggedness. Another bug is forgetting to allocate result[r] at all, causing a NullPointerException when you try result[r][c] = ....

Interpreting common AP-style 2D array prompts

Many 2D array FRQ-style prompts come disguised as real situations:

  • Image processing: a 2D array of pixel values; apply a filter; detect edges.
  • Game board: check winners, count pieces, validate moves.
  • Seating chart / theater: find available seats, compute row occupancy.

In all of them, your job is the same: translate the story into indices and loops.

Worked example: check if any row is “full”

Suppose a theater seating chart uses "X" for occupied and "." for empty. Determine whether any row is completely full.

public static boolean hasFullRow(String[][] seats) {
    for (int r = 0; r < seats.length; r++) {
        boolean full = true;
        for (int c = 0; c < seats[r].length; c++) {
            if (!"X".equals(seats[r][c])) {
                full = false;
                break;
            }
        }
        if (full) {
            return true;
        }
    }
    return false;
}

Why "X".equals(seats[r][c]) instead of seats[r][c].equals("X")? If a seat cell could be null, calling .equals on it would throw a NullPointerException. Putting the literal on the left is safer.

What goes wrong most often in 2D algorithms

When students lose points, it’s usually due to one of these categories:

  • Wrong bounds: using the wrong .length in one loop.
  • Wrong dimension interpretation: mixing up rows and columns.
  • Incorrect accumulator reset: forgetting to reset sum inside the row loop.
  • Unsafe neighbor access: not checking edges before using r - 1, r + 1, c - 1, or c + 1.
  • Mutating when you meant to compute (or vice versa): returning a value when you were supposed to modify the grid, or modifying the grid when the prompt implied it should remain unchanged.
Exam Focus
  • Typical question patterns:
    • Write a method that computes something from a 2D array (sum, average, count with a condition, max/min with location).
    • Write a method that modifies a 2D array in place based on a rule (replace, update, apply constraints).
    • Write a method that inspects patterns (full rows/columns, neighbor comparisons, border conditions).
  • Common mistakes:
    • Returning too early inside nested loops (for example, returning false after the first non-match instead of checking all cells).
    • Incorrect edge handling when referencing neighbors (leading to index errors).
    • Using == to compare String values instead of .equals, which can produce unpredictable results.