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.
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; // 402What 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).
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.
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
Swap two rows
In "A grid", swap rows 0 and 1 of
seatswith three lines (int[] tmp = seats[0]; seats[0] = seats[1]; seats[1] = tmp;). Print withdeepToString. Notice no element was copied: only two references moved. - 2
Print a 2D array the wrong way
Print
Arrays.toString(grid)instead ofdeepToString. You'll see three[I@...entries, one reference per row. Switch back. - 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
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]Expected output
[1]
[1, 1]
[1, 2, 1]
[1, 3, 3, 1]
[1, 4, 6, 4, 1]
[1, 5, 10, 10, 5, 1]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: trueBreak 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.
public class Main {
public static void main(String[] args) {
int[][] grid = new int[3][];
grid[1][0] = 5;
}
}Break #2
Use the row count as the column bound
In a 3 x 2 grid, loop columns with c < grid.length.
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;
}
}
}
}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 asr * cols + cis 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 themultianewarraybytecode, which allocates the outer array and all rows in one instruction;new int[3][]usesanewarrayfor the outer array only.
Remember this
- 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.lengthis the number of rows;grid[r].lengthis the number of columns in rowr. - 2
Under the hood,
gridrefers to an outer array of 3 references, and each reference points to a separateint[4]row array on the heap. Sogrid[1]is itself anint[]you can pass around, print withArrays.toString, or replace. - 3
Because rows are separate arrays, they can have different lengths: a jagged array.
new int[3][]creates only the outer array with threenullrows; you create each row yourself, for exampletri[r] = new int[r + 1]. - 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 overc < grid[r].length. Usinggrid[r].length(notgrid[0].length) makes the loop correct for jagged arrays too. - 5
Initializers nest:
int[][] seats = {{1, 2, 3}, {4, 5, 6}};. Print withArrays.deepToString, compare withArrays.deepEquals. And remember:clone()on a 2D array copies only the outer array, so the rows are shared (a shallow copy). - 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
How is int[][] grid = new int[3][4]; laid out in memory?
What is a jagged array, and why can Java have one?
What's the difference between Arrays.toString and Arrays.deepToString on a 2D array, and between equals and deepEquals?
Why does grid.clone() not fully copy a 2D array, and how do you copy it fully?
Practice
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.
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}}.
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 flatint[]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
deepEqualscorrectly.