Command Palette

Search for a command to run...

PHASE 3Beginner ~27 min· topic 9 of 11

Topic 3.9

Two-Dimensional and Jagged Arrays

In one line

Java has no true grid type: a two-dimensional array is an array whose elements are references to other arrays (the rows). That means rows can have different lengths (jagged arrays), and you index with grid[row][col].

Think of it like this

A cinema. The cinema has numbered rows, and each row has numbered seats. To find a seat you need two numbers: row 3, seat 7. In an ordinary cinema every row has the same number of seats, but in some theatres the front rows are shorter. A 2D array is the cinema; a jagged array is the theatre with rows of different lengths.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

Two-dimensional (2D) array
An array of arrays, used like a table with rows and columns.
Row
One inner array of a 2D array, reached with one index, like grid[1].
Column
The position within a row, the second index in grid[row][col].
Jagged array
A 2D array whose rows have different lengths. Java allows it because each row is a separate array.
Row-major order
Visiting a grid row by row: all of row 0, then all of row 1, and so on.
Matrix
A rectangular grid of numbers, where every row has the same length.
Deep copy
A copy where every nested array (each row) is copied too, so nothing is shared with the original.

Step by step

01Create a grid and index it

int[][] grid = new int[3][4]; reads as "3 rows, each with 4 ints". The first index picks the row, the second the column: grid[1][2] = 7; sets row 1, column 2.

With an initializer, each inner pair of braces is one row: int[][] seats = {{1, 2, 3}, {4, 5, 6}}; has 2 rows of 3.

Main.javawhole filejava
int[][] grid = new int[3][4];   // 3 rows, 4 columns, all 0
grid[1][2] = 7;                 // row 1, column 2
int rows = grid.length;         // 3
int cols = grid[0].length;      // 4

02What it really looks like in memory

There are four array objects here, not one. The variable grid points to an outer array of length 3. Each of its elements is a reference to a separate row array of length 4.

grid[1][2] therefore takes two steps: follow grid to the outer array and read slot 1 (a reference), then follow that to the row array and read slot 2.

This also explains the [[I type name you saw in Topic 3.8: an array ([) of int arrays ([I).

What it really looks like in memorydiagram
Rendering diagram…

03Visiting every cell

Nested loops, rows outside and columns inside, visit cells in row-major order. Use grid[r].length for the inner bound so the code works whatever the row lengths are.

With enhanced for loops: for (int[] row : grid) for (int v : row) .... The outer loop variable is a whole row, an int[].

Row-major order is also the fast order: each row's elements are next to each other in memory, so the CPU cache works well. Going column by column jumps between different row arrays on every step.

04Jagged arrays: rows of different lengths

Leave the second size out, new int[5][], and Java creates only the outer array, with every row null. You then create each row with whatever length it needs.

Pascal's triangle is the classic example: row r has r + 1 numbers. A jagged array stores exactly those, with no wasted cells. Forgetting to create a row and then using it gives a NullPointerException.

Jagged arrays: rows of different lengthsdiagram
Rendering diagram…

05Rows are references: sharing and copying

int[] row = grid[0]; doesn't copy the row; it's a second name for it. Changing row[0] changes grid[0][0].

Swapping two rows is cheap (swap two references in the outer array) no matter how long the rows are.

grid.clone() makes a new outer array but shares the row arrays. To copy everything, clone each row: copy[r] = grid[r].clone();. That's a deep copy for a 2D array of primitives.

06Printing and comparing

Arrays.toString(grid) prints the outer array's elements, which are references: [[I@1b6d3586, ...]. Use Arrays.deepToString(grid) to print the numbers.

Likewise, Arrays.equals(a, b) on 2D arrays compares the row references; two separately built but identical grids are not equal that way. Arrays.deepEquals(a, b) compares the contents.

07More dimensions

new int[2][3][4] is an array of 2 arrays of 3 arrays of 4 ints: 1 + 2 + 6 = 9 array objects. The same rules apply at every level.

Beyond two dimensions, code gets hard to read quickly. Often a small class (a Cell record, a Grid class with get(row, col)) makes intent clearer (Phase 4).

Try it yourself

  1. 1

    Swap two rows

    In "A grid", swap rows 0 and 1 of seats with three lines (int[] tmp = seats[0]; seats[0] = seats[1]; seats[1] = tmp;). Print with deepToString. Notice no element was copied: only two references moved.

  2. 2

    Print a 2D array the wrong way

    Print Arrays.toString(grid) instead of deepToString. You'll see three [I@... entries, one reference per row. Switch back.

  3. 3

    Forget to create a row

    In the Pascal example, comment out tri[r] = new int[r + 1]; and predict the error before running. Then restore it.

Code & diagrams

A grid: create, set, loop, print New tab
Sign in to run this example in your browser.

Expected output

rows = 3, cols = 4
[[0, 0, 0, 0], [0, 0, 7, 0], [0, 0, 0, 0]]
row 1 = [0, 0, 7, 0]
1 2 3 
4 5 6 
total = 21
row length 3: [1, 2, 3]
row length 3: [4, 5, 6]
A jagged array: Pascal's triangle New tab
Sign in to run this example in your browser.

Expected output

[1]
[1, 1]
[1, 2, 1]
[1, 3, 3, 1]
[1, 4, 6, 4, 1]
[1, 5, 10, 10, 5, 1]
Transpose, shared rows and deep copies New tab
Sign in to run this example in your browser.

Expected output

transpose: [[1, 4], [2, 5], [3, 6]]
m       = [[99, 2, 3], [4, 5, 6]]
shallow = [[99, 2, 3], [4, 5, 6]]
deep    = [[99, 2, 3], [-1, 5, 6]]
equals: false, deepEquals: true

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

Use a row before creating it

Create a jagged array with new int[3][] and write into a row straight away.

Main.javawhole filejava
public class Main {
    public static void main(String[] args) {
        int[][] grid = new int[3][];
        grid[1][0] = 5;
    }
}
terminal
$ java Main
── what you'll see ──
Exception in thread "main" java.lang.NullPointerException: Cannot store to int array because "<local1>[1]" is null
at Main.main(Main.java:4)

Break #2

Use the row count as the column bound

In a 3 x 2 grid, loop columns with c < grid.length.

Main.javawhole filejava
public class Main {
    public static void main(String[] args) {
        int[][] grid = new int[3][2];
        for (int r = 0; r < grid.length; r++) {
            for (int c = 0; c < grid.length; c++) {
                grid[r][c] = r + c;
            }
        }
    }
}
terminal
$ java Main
── what you'll see ──
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: Index 2 out of bounds for length 2
at Main.main(Main.java:6)

Myth vs fact

Myth

new int[3][4] is one block of 12 ints, like in C.

Fact

It's four separate array objects: one outer array of references plus three rows of 4 ints.

Myth

All rows of a 2D array have the same length.

Fact

Only if you make them so. Each row is an independent array, so jagged arrays are normal Java.

Myth

clone() copies a 2D array completely.

Fact

It copies only the outer array. The rows are shared until you clone each one.

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    Every row is a separate heap object with its own 16-byte header, and rows may be scattered in memory. For large numeric grids, a single flat array int[rows * cols] indexed as r * cols + c is more compact and faster to scan; many image and matrix libraries do exactly this.

  • ▸

    Traversal order matters: summing a 4000 x 4000 int[][] row by row reads memory sequentially, while column by column touches a different row array on every step and can be several times slower because of CPU cache misses. Measure with a benchmark harness such as JMH, not with a single timed run.

  • ▸

    new int[3][4] compiles to the multianewarray bytecode, which allocates the outer array and all rows in one instruction; new int[3][] uses anewarray for the outer array only.

Remember this

  1. 1

    int[][] grid = new int[3][4]; makes 3 rows of 4 columns, all zeros. Read and write with two indexes: grid[row][col]. grid.length is the number of rows; grid[r].length is the number of columns in row r.

  2. 2

    Under the hood, grid refers to an outer array of 3 references, and each reference points to a separate int[4] row array on the heap. So grid[1] is itself an int[] you can pass around, print with Arrays.toString, or replace.

  3. 3

    Because rows are separate arrays, they can have different lengths: a jagged array. new int[3][] creates only the outer array with three null rows; you create each row yourself, for example tri[r] = new int[r + 1].

  4. 4

    The usual way to visit every element is row by row with nested loops: the outer loop over r < grid.length, the inner loop over c < grid[r].length. Using grid[r].length (not grid[0].length) makes the loop correct for jagged arrays too.

  5. 5

    Initializers nest: int[][] seats = {{1, 2, 3}, {4, 5, 6}};. Print with Arrays.deepToString, compare with Arrays.deepEquals. And remember: clone() on a 2D array copies only the outer array, so the rows are shared (a shallow copy).

  6. 6

    Grids are everywhere in problems: game boards, images, spreadsheets, maps, dynamic programming tables. The DSA course works with them in its Arrays and Dynamic Programming modules (/dsa/arrays, /dsa/dp-grids).

Explain it without notes

01

How is int[][] grid = new int[3][4]; laid out in memory?

02

What is a jagged array, and why can Java have one?

03

What's the difference between Arrays.toString and Arrays.deepToString on a 2D array, and between equals and deepEquals?

04

Why does grid.clone() not fully copy a 2D array, and how do you copy it fully?

Practice

01

Write a program that builds a 4 x 4 multiplication table in an int[][] (cell = (r + 1) * (c + 1)) and prints it row by row, values separated by spaces.

02

Write static int[] rowSums(int[][] m) that returns the sum of each row. Test it on the jagged array {{1, 2}, {3, 4, 5}, {6}}.

03

Write a program that finds the largest value and its position in {{3, 8, 1}, {9, 2, 7}, {4, 6, 5}} and prints max 9 at row 1, col 0.

Trade-offs

  • ↔

    int[][] is easy to read (grid[r][c]) and supports jagged rows, but costs an object per row and may scatter rows in memory. A flat int[] with index arithmetic is faster and smaller for big rectangular grids, at some cost in readability.

  • ↔

    Jagged arrays save memory when row sizes really differ (triangles, adjacency lists), but every loop must use each row's own length.

Done when you can

  • Done when you can create, fill and print a 2D array with deepToString.

  • Done when you can draw the outer array and row arrays of a 2D array in memory.

  • Done when you can build a jagged array and loop over it safely.

  • Done when you can explain shallow vs deep copies of 2D arrays and write a deep copy.

  • Done when you can use deepEquals correctly.