JAVA / LOOPS AND ARRAYS
Multidimensional arrays and ragged rows
Build, fill and safely traverse Java 2D arrays whose rows have different lengths, and tell an unallocated null row from a genuine zero-length row.
What you will learn
- Read int[][] as an array of row references, not as a fixed rectangle
- Allocate with new int[n][] and give each row its own length
- Bound inner loops with grid[r].length, never grid[0].length or a constant
- Distinguish a null row (NullPointerException) from a length-0 row (0 iterations)
Understanding Multidimensional arrays and ragged rows
Java has no rectangular two-dimensional array type. int[][] is a one-dimensional array whose elements happen to be references to int[] objects, so grid[r] hands you a whole row and grid[r][c] is two separate index operations: fetch the row reference, then index inside that row. Because each row is its own heap object carrying its own length field, nothing in the language links the lengths of different rows to each other.
new int[3][4] looks like it declares a fixed rectangle, but the compiler expands it into four allocations: one outer array of three references plus three separate int[4] objects. Omit the last size, as in new int[3][], and only the outer array is created; every slot starts as null and you decide row by row how long its array should be. Ragged rows are therefore not a feature bolted on to arrays, they are the natural state of an array of arrays, and the rectangular case is just what the two-size shorthand happens to build.
This changes how you loop. The outer bound is grid.length, the number of rows, which says nothing about widths; the inner bound has to be grid[r].length, re-read for every row. Code that hoists one width out of the loop works by luck on rectangular data and breaks on the first shorter row, and a row you never assigned fails in a different way again: indexing null throws NullPointerException rather than an out-of-range error, which tells you the allocation is missing, not the index.
public class Main {
public static void main(String[] args) {
int[][] tri = new int[4][]; // 4 row slots, every one still null
System.out.println("row 0 before: " + tri[0]);
for (int r = 0; r < tri.length; r++) {
tri[r] = new int[r + 1]; // each row picks its own length
tri[r][0] = 1;
tri[r][r] = 1;
for (int c = 1; c < r; c++) {
tri[r][c] = tri[r - 1][c - 1] + tri[r - 1][c];
}
}
for (int r = 0; r < tri.length; r++) {
System.out.println("length " + tri[r].length + " -> " + java.util.Arrays.toString(tri[r]));
}
System.out.println("outer length: " + tri.length);
System.out.println("outer class: " + tri.getClass().getName());
System.out.println("row class: " + tri[0].getClass().getName());
}
}An int[][] is an array of references to independent int[] objects, so every row carries its own length.
Worked examples
A rectangular array is not locked into being rectangular
Shows that new int[2][3] only builds rows that happen to match, and that any row can later be swapped for a shorter one.
import java.util.Arrays;
public class Main {
public static void main(String[] args) {
int[][] grid = new int[2][3];
System.out.println(Arrays.deepToString(grid));
grid[1] = new int[] {7, 8};
System.out.println(Arrays.deepToString(grid));
System.out.println(grid[0].length + " " + grid[1].length);
int cells = 0;
for (int[] row : grid) {
cells += row.length;
}
System.out.println("cells: " + cells);
}
}Example explained
Line 1new int[2][3] allocates one outer array of two references plus two independent int[3] objects, all zero-filled.
Line 2grid[1] = new int[] {7, 8} replaces the whole second row object, and no check compares it against the width of row 0, so the array is now ragged.
Line 3Arrays.deepToString recurses into each row; Arrays.toString would print the rows' identity hashes instead of their values.
Line 4row.length reports 3 then 2, so the cell count is 5, not grid.length * 3.
Traversing rows of unequal length
Builds a ragged String[][] from nested initialisers, including an empty row, and walks it with a per-row inner bound.
public class Main {
public static void main(String[] args) {
String[][] rows = {
{"ant"},
{"bee", "bat"},
{},
{"cow", "cat", "carp"}
};
for (int r = 0; r < rows.length; r++) {
System.out.print(r + " (" + rows[r].length + "):");
for (int c = 0; c < rows[r].length; c++) {
System.out.print(" " + rows[r][c]);
}
System.out.println();
}
}
}Example explained
Line 1The nested braces create four separate row objects of length 1, 2, 0 and 3; rows.length is 4, the row count only.
Line 2{} is a real zero-length array rather than null, so rows[2].length is 0 and reading it is safe.
Line 3The inner condition c < rows[r].length is re-evaluated for each r, so the third row simply runs zero iterations and the line stops after the colon.
Line 4Hard-coding 3 as the inner bound would throw ArrayIndexOutOfBoundsException on the very first row.
Important notes
Arrays.toString(grid) on an int[][] prints entries like [I@1b6d3586 because it calls toString on each row reference; use Arrays.deepToString for the contents.
Rows stay ordinary objects even after new int[3][4], so grid[1] = grid[0] makes two slots point at one row and writes through either index are visible in both.
Common mistakes
Writing new int[][3] to ask for three columns: it does not compile ('array dimension missing'), because only trailing dimensions may be left empty, as in new int[3][].
Using grid[0].length or a COLS constant as the inner bound: the first shorter row throws ArrayIndexOutOfBoundsException, and any longer row silently loses its trailing cells.
Treating rows from new int[4][] as usable: grid[0][0] = 1 throws NullPointerException because that slot still holds null, and beginners then hunt for an index bug that is really a missing allocation.
Try it yourself
Change, predict, then run
Create char[][] grid = new char[5][] and fill row r with the first r + 1 lowercase letters starting at 'a', then print each row with System.out.println(new String(grid[r])) so the output forms a staircase from a to abcde.
Open the Java workspaceCheck your understanding
Given int[][] a = new int[3][]; a[0] = new int[2]; a[1] = new int[0]; then System.out.println(a.length + " " + a[1].length); then a[2][0] = 5; what happens?
- Prints "3 0", then throws NullPointerException
- Prints "3 0", then throws ArrayIndexOutOfBoundsException
- Prints "3 2", then throws ArrayIndexOutOfBoundsException
- Prints "3 0" and finishes normally, because a[2] defaults to a zero-filled row
Show answer
a.length is 3 (row slots) and a[1] is a real zero-length array, so its length prints as 0. a[2] was never assigned, so it still holds null; a[2][0] must dereference that reference before any bounds check can happen, which is why it throws NullPointerException. ArrayIndexOutOfBoundsException is tempting because index 0 looks out of range for something empty, but there is no array object there to have a range, and new int[3][] never fills the slots for you.