Multidimensional & Jagged Arrays
Java has no true native multidimensional array — a 2D array is really an array of arrays, which is exactly why rows are free to have different lengths (jagged arrays).
What looks like a multidimensional array in Java — int[][] grid — is, underneath, an array whose elements are themselves arrays. This "array of arrays" model is why Java allows jagged arrays, where each row can have a different length, unlike languages with genuinely rectangular native multidimensional arrays. Understanding this model demystifies both rectangular grids and jagged structures, since they're really the same mechanism used two different ways.
Arrays of Arrays — the Real Model
int[][] grid declares a variable whose type is "array of int[]" — each element of grid is itself a reference to a separate int[] array object. There is no single contiguous rectangular memory block the way there might be in a language with true native multidimensional arrays; instead you have one outer array holding references to some number of inner arrays.
new int[3][4] is convenience syntax that both creates the outer array of length 3 AND creates each of the three inner arrays with length 4 automatically.
int[][] grid = new int[3][4]; // 3 rows, each row an independent int[4] array
System.out.println(grid.length); // 3 — number of rows (outer array length)
System.out.println(grid[0].length); // 4 — length of the first row (inner array)
System.out.println(grid[0] == grid[1]); // false — distinct array objects, even though same lengthRectangular (Regular) 2D Arrays
When every row has the same length, the array is often called "rectangular," even though it's still implemented as an array of independent row arrays under the hood. This is the common case for grids, matrices, and tables.
int[][] matrix = {
{1, 2, 3},
{4, 5, 6},
{7, 8, 9}
};
System.out.println(matrix[1][2]); // 6 — row index 1, column index 2Jagged Arrays
Because each row is an independent array object, rows are free to have different lengths — this is a "jagged" (or "ragged") array. You can create the outer array first, sized for however many rows you need, and then assign each row its own independently sized array.
new int[4][] leaves the inner arrays unassigned (null) until you explicitly create each row — this is exactly how jagged arrays are built up.
int[][] triangle = new int[4][]; // 4 rows, lengths not yet decided
triangle[0] = new int[]{1};
triangle[1] = new int[]{1, 1};
triangle[2] = new int[]{1, 2, 1};
triangle[3] = new int[]{1, 3, 3, 1};
for (int[] row : triangle) {
System.out.println(java.util.Arrays.toString(row));
}
// [1]
// [1, 1]
// [1, 2, 1]
// [1, 3, 3, 1]Initialization Styles
Multidimensional arrays can be initialized with nested literals, with new giving explicit dimensions for a rectangular shape, or with new giving only the outer dimension for a jagged shape you build up row by row.
// Nested literal — most concise, works for jagged shapes too
int[][] a = {{1, 2}, {3, 4, 5}, {6}};
// Fully-specified rectangular
int[][] b = new int[2][3]; // every row is int[3], all zeros
// Outer dimension only — must fill in rows yourself
int[][] c = new int[2][];
c[0] = new int[5];
c[1] = new int[2];Nested Indexing and Traversal
Access an element with two (or more) bracket pairs in sequence: grid[row][col]. To traverse a 2D array fully, nest a loop over rows inside a loop over columns — but since rows can have independent lengths, always ask each row for its own .length rather than assuming a fixed column count.
int[][] jagged = {{1, 2, 3}, {4}, {5, 6}};
for (int r = 0; r < jagged.length; r++) {
for (int c = 0; c < jagged[r].length; c++) { // note: jagged[r].length, not a fixed constant
System.out.print(jagged[r][c] + " ");
}
System.out.println();
}
// 1 2 3
// 4
// 5 6
// Enhanced for-loop version — often cleaner
for (int[] row : jagged) {
for (int value : row) {
System.out.print(value + " ");
}
System.out.println();
}Arrays of Three or More Dimensions
The same array-of-arrays idea extends to any depth: int[][][] cube is an array of int[][], each of which is an array of int[]. Higher dimensions are used far less often, but they follow identical rules — each level is an independent array object, and jaggedness is possible at every level.
int[][][] cube = new int[2][3][4];
cube[1][2][3] = 99;
System.out.println(cube.length); // 2
System.out.println(cube[0].length); // 3
System.out.println(cube[0][0].length); // 4Memory Model, Informally
A typical JVM implementation stores the outer array as a block of references, each pointing to a separately-allocated inner array elsewhere on the heap — this is a JVM implementation detail rather than something the Java Language Specification dictates in low-level terms, but it's a useful mental picture that directly explains why rows can have different lengths and different identities.
grid is an outer array of references; each row is a separately allocated inner array, which is exactly why rows can differ in length.
Practical Usage
2D arrays are the natural representation for grids, matrices, images (as arrays of pixel rows), board games, and dynamic-programming tables. Jagged arrays fit naturally where each row's size genuinely differs — a school's list of class rosters, a triangular data structure like Pascal's triangle, or an adjacency list representation of a graph.
static int[][] adjacencyList(int nodeCount, int[][] edges) {
int[][] degree = new int[nodeCount][]; // will size each row to its actual neighbor count
int[] counts = new int[nodeCount];
for (int[] e : edges) { counts[e[0]]++; }
for (int i = 0; i < nodeCount; i++) degree[i] = new int[counts[i]];
return degree; // simplified illustration of a jagged structure sized per node
}Edge Cases
new int[3][] creates an outer array of three null inner-array references — accessing grid[0][0] before assigning grid[0] throws NullPointerException, not ArrayIndexOutOfBoundsException, because you're dereferencing a null row. A rectangular-looking array built entirely from new int[r][c] never has this problem, since every row is auto-created.
int[][] rows = new int[2][];
try {
System.out.println(rows[0][0]); // rows[0] is null here
} catch (NullPointerException e) {
System.out.println("caught: row was never assigned");
}Best Practices
Use new int[r][c] when you genuinely need a fixed rectangular shape — it's simpler and avoids null-row bugs. Reach for new int[r][] plus manual row assignment (or nested literals) only when rows truly need independent lengths. Always index a row's own .length in loops over jagged data rather than assuming a shared column count. Use Arrays.deepToString/Arrays.deepEquals for printing and comparing multidimensional arrays — plain Arrays.toString on a 2D array just prints the inner arrays' identity hashes, not their contents.
int[][] grid = {{1, 2}, {3, 4}};
System.out.println(java.util.Arrays.toString(grid)); // [[I@6b95977, [I@... ] — useless
System.out.println(java.util.Arrays.deepToString(grid)); // [[1, 2], [3, 4]] — correctComplete Example
import java.util.Arrays;
public class JaggedDemo {
public static void main(String[] args) {
// Pascal's triangle, built as a jagged array
int rowCount = 5;
int[][] triangle = new int[rowCount][];
for (int r = 0; r < rowCount; r++) {
triangle[r] = new int[r + 1];
triangle[r][0] = 1;
triangle[r][r] = 1;
for (int c = 1; c < r; c++) {
triangle[r][c] = triangle[r - 1][c - 1] + triangle[r - 1][c];
}
}
for (int[] row : triangle) {
System.out.println(Arrays.toString(row));
}
// [1]
// [1, 1]
// [1, 2, 1]
// [1, 3, 3, 1]
// [1, 4, 6, 4, 1]
}
}Key Takeaways
A Java 2D array is really an array of arrays — there is no separate native rectangular multidimensional array type.
Because each row is an independent array object, rows can have different lengths — this is what makes jagged arrays possible.
new int[r][c] creates a full rectangular grid with all rows auto-allocated; new int[r][] creates only the outer array, leaving rows null until you assign them.
Accessing an unassigned row of a partially built jagged array throws NullPointerException, not ArrayIndexOutOfBoundsException.
Use Arrays.deepToString/deepEquals for printing and comparing multidimensional arrays; plain Arrays.toString/equals only look one level deep.
int[][] grid = new int[3][]; // outer array only System.out.println(grid[0][0]); // NullPointerException — row never assigned int[][] grid = new int[3][4]; // fully rectangular, all rows auto-created System.out.println(grid[0][0]); // 0, safely new int[3][] leaves each row as null until you explicitly assign it; if you need every row pre-populated, use the fully-dimensioned form new int[3][4] instead.
for (int c = 0; c < grid[0].length; c++) {... } // assumes every row shares row 0's length for (int r = 0; r < grid.length; r++) { for (int c = 0; c < grid[r].length; c++) {... } // ask each row for its own length }
In a jagged array, rows can have different lengths — hardcoding one row's length as the loop bound for every row either misses elements or throws ArrayIndexOutOfBoundsException on shorter/longer rows.
System.out.println(Arrays.toString(grid)); // prints [[I@..., [I@...] for a 2D array
System.out.println(Arrays.deepToString(grid)); // prints actual nested contents
Arrays.toString only formats the immediate elements — for a 2D array, those elements are inner array references, so you get their identity-hash-based default string, not their contents. Use deepToString (and deepEquals for comparison) for nested arrays.
What is a Java 2D array actually implemented as, and why does this explain why jagged arrays are possible?
It is an array of arrays — the outer array holds references to independently allocated inner arrays. Because each row is its own separate array object, nothing forces rows to share the same length, which is exactly what makes jagged (ragged) arrays possible.
What is the difference between new int[3][4] and new int[3][] in terms of what gets created?
new int[3][4] creates the outer array of length 3 AND automatically creates all three inner arrays, each of length 4, filled with default int values. new int[3][] creates only the outer array of length 3, with each of its three elements initialized to null until you explicitly assign a row array to each one.
What exception is thrown by accessing grid[0][0] on a int[][] grid = new int[3][]; before any row has been assigned, and why that exception rather than an array-bounds exception?
It throws NullPointerException, because grid[0] itself is null (never assigned an inner array) — the code is trying to index into a null reference, which is a null-dereference error, not an out-of-bounds error on a real array.
Why is it unsafe to use one row's length as the loop bound for iterating columns across all rows of a jagged array?
Because rows in a jagged array can have different lengths; using a fixed row's length as the shared column bound either skips elements on longer rows or throws ArrayIndexOutOfBoundsException on shorter rows. Each row should be indexed using its own .length.
Why does Arrays.toString on a 2D array print something like [[I@6b95977, [I@232204a1] instead of the actual nested numbers?
Arrays.toString only formats the array's immediate elements. For a 2D array, those immediate elements are references to inner int[] arrays, and since arrays don't override toString(), each inner array prints its default Object-style identity representation instead of its contents. Arrays.deepToString recurses into nested arrays and formats them correctly.
How would you build a triangular jagged array where row i has exactly i+1 elements, for i from 0 to 4?
Create the outer array with new int[5][], then loop from r = 0 to 4, assigning triangle[r] = new int[r + 1] on each iteration — giving row 0 a length of 1, row 1 a length of 2, and so on up to row 4 with length 5.
Is grid[0] == grid[1] ever true for a 2D array created with new int[3][4], and why or why not?
No — even though new int[3][4] gives every row the same length, each row is still a separately allocated array object with its own identity, so grid[0] == grid[1] compares two distinct references and is always false (unless you explicitly assign the same array reference to both, e.g. grid[1] = grid[0]).
Write a method transpose that returns the transpose of a rectangular int[][] matrix (rows become columns).
import java.util.Arrays; public class TransposeDemo { static int[][] transpose(int[][] matrix) { int rows = matrix.length; int cols = matrix[0].length; int[][] result = new int[cols][rows]; for (int r = 0; r < rows; r++) { for (int c = 0; c < cols; c++) { result[c][r] = matrix[r][c]; } } return result; } public static void main(String[] args) { int[][] m = {{1, 2, 3}, {4, 5, 6}}; int[][] t = transpose(m); for (int[] row: t) System.out.println(Arrays.toString(row)); // [1, 4] // [2, 5] // [3, 6] } } transpose swaps rows and columns: result has as many rows as the original had columns and vice versa, and result[c][r] = matrix[r][c] for every valid r, c — turning a 2x3 matrix into a 3x2 one.
What does this print, and why?
It prints 99. b = a copies only the outer array reference, so a and b refer to the exact same outer array (and thus the exact same row arrays too) — mutating b[0][1] mutates the very same int[] row that a[0] refers to, so the change is visible through a as well.
What happens when this loop runs, and why?
It prints 2, then 1, then throws NullPointerException on the third iteration. jagged[2] was never assigned an inner array, so it remains null; when the enhanced for-loop binds row to that null reference and the code calls row.length, it dereferences null, which throws NullPointerException rather than printing 0 or skipping the element.
int[][] a = new int[2][3];
a[0][1] = 5;
int[][] b = a;
b[0][1] = 99;
System.out.println(a[0][1]);int[][] jagged = new int[3][];
jagged[0] = new int[]{1, 2};
jagged[1] = new int[]{3};
// jagged[2] is never assigned
for (int[] row : jagged) {
System.out.println(row.length);
}