A 2D array in Java is an array whose elements are themselves arrays, which means you can think of it as a grid of rows and columns. You declare it with two bracket pairs, index it with two subscripts, and traverse it with two nested loops. Understanding that it is really "an array of array references" explains almost every surprise you will meet later, including jagged rows and the way one row can quietly change when you edit another.
What is a 2D array really?
Java has no special 2D array type. int[][] is shorthand for "an array whose element type is int[]". The outer array holds references to inner arrays; the inner arrays hold the actual int values. Two consequences follow immediately. First, the outer array knows its own length, but each row can have its own length. Second, storing a row is not the same as storing a copy of a row.
int[][] grid = new int[3][4];The single expression on the right allocates two things: an array of three int[] references, and three separate int[4] arrays attached to those references. Every cell starts at zero because int has a default value of zero. The whole allocation happens in one statement, but it is two levels of allocation underneath.
How do you fill a grid with values?
Filling a grid means walking every row and every column of that row, because the two lengths are independent. The nested loop below is the standard shape. Note the inner bound is grid[row].length and not grid[0].length - using the first row's length is the classic bug that throws an exception on the first ragged row.
public class GridBasics {
public static void main(String[] args) {
int[][] grid = new int[3][4];
for (int row = 0; row < grid.length; row++) {
for (int col = 0; col < grid[row].length; col++) {
grid[row][col] = (row + 1) * 10 + col;
}
}
System.out.println("rows: " + grid.length);
System.out.println("columns in row 0: " + grid[0].length);
for (int[] row : grid) {
StringBuilder line = new StringBuilder();
for (int value : row) {
line.append(String.format("%4d", value));
}
System.out.println(line);
}
System.out.println("grid[1][2] = " + grid[1][2]);
}
}rows: 3
columns in row 0: 4
10 11 12 13
20 21 22 23
30 31 32 33
grid[1][2] = 22The output shows the shape of the data: three rows, four columns each, values that make the row and column positions obvious. The final line reads a single cell, which is the whole access syntax in miniature: grid[1][2] means "the second element of the outer array, then the third element of that inner array".
What are jagged arrays and when do you want them?
A jagged array is a 2D array whose rows have different lengths. You build one by giving the outer array its length only, then assigning a fresh inner array to each row. This is the natural shape whenever rows come from different sources: a seating plan where each row has a different number of seats, a CSV where the first record has four fields and the rest have three, a table of variable-length tags.
public class JaggedGrid {
public static void main(String[] args) {
String[][] tickets = new String[3][];
tickets[0] = new String[] { "A12", "A13" };
tickets[1] = new String[] { "B20" };
tickets[2] = new String[] { "C31", "C32", "C33" };
for (int i = 0; i < tickets.length; i++) {
System.out.println("row " + i + " has " + tickets[i].length + " seats");
for (String seat : tickets[i]) {
System.out.print(seat + " ");
}
System.out.println();
}
System.out.println("total seats: " + count(tickets));
}
static int count(String[][] rows) {
int total = 0;
for (String[] row : rows) {
total += row.length;
}
return total;
}
}row 0 has 2 seats
A12 A13
row 1 has 1 seats
B20
row 2 has 3 seats
C31 C32 C33
total seats: 6Two rules keep jagged arrays safe. Never assume row.length is the same for every row, and never touch grid[i] before you have assigned an array to it. The second mistake is worth spelling out because the failure is confusing: the outer slot holds null until you fill it, and reading through null throws a NullPointerException that points at your loop rather than at the real mistake.
How do you traverse a grid and compute from it?
Two loop styles cover most work. The index form gives you both coordinates, which you need when the answer depends on position. The for-each form is shorter when you only care about the values. A third habit is worth forming early: pull the row-sum and column-sum logic into their own methods, so the traversal code stays about traversal.
public class GridMath {
public static void main(String[] args) {
int[][] sales = {
{ 120, 80, 45 },
{ 200, 190, 75 }
};
int total = 0;
int best = Integer.MIN_VALUE;
int bestRow = -1;
for (int row = 0; row < sales.length; row++) {
for (int col = 0; col < sales[row].length; col++) {
total += sales[row][col];
if (sales[row][col] > best) {
best = sales[row][col];
bestRow = row;
}
}
}
System.out.println("total sales: " + total);
System.out.println("best cell: " + best + " in row " + bestRow);
System.out.println("row 0 sum: " + rowSum(sales[0]));
System.out.println("column 2 sum: " + columnSum(sales, 2));
}
static int rowSum(int[] row) {
int sum = 0;
for (int value : row) {
sum += value;
}
return sum;
}
static int columnSum(int[][] grid, int column) {
int sum = 0;
for (int[] row : grid) {
if (column < row.length) {
sum += row[column];
}
}
return sum;
}
}total sales: 710
best cell: 200 in row 1
row 0 sum: 245
column 2 sum: 120The columnSum method contains a guard you should copy: it checks column < row.length before reading. In a rectangular grid that check is never false, but in a jagged one it is the difference between a total and a crash. Whenever a column-oriented calculation meets a jagged array, that guard belongs in the code.
What are the three traps people fall into?
Trap one is an index past the end of a row, which throws ArrayIndexOutOfBoundsException. Trap two is reading a row that was never created, which throws NullPointerException. Trap three is subtler: two outer slots pointing at the same inner array, so a write through one is visible through the other.
public class GridTraps {
public static void main(String[] args) {
int[][] grid = new int[2][2];
try {
grid[0][5] = 1;
} catch (ArrayIndexOutOfBoundsException e) {
System.out.println("out of bounds: " + e.getMessage());
}
int[][] halfBuilt = new int[2][];
try {
System.out.println(halfBuilt[0][0]);
} catch (NullPointerException e) {
System.out.println("row was never created: " + e.getClass().getSimpleName());
}
int[][] shared = new int[2][2];
int[] alias = shared[0];
alias[1] = 99;
System.out.println("shared[0][1] after writing through the alias = " + shared[0][1]);
}
}out of bounds: Index 5 out of bounds for length 2
row was never created: NullPointerException
shared[0][1] after writing through the alias = 99That third case is the one that turns a passing test into a mystery. The variable alias and the row shared[0] are two names for one array, so the write of 99 shows up when you read shared[0][1]. If you want two independent rows, you must allocate twice: shared[0] = new int[2]; shared[1] = new int[2];.
When should you reach for a 2D array at all?
A 2D array is the right tool when the data is genuinely a rectangle and the coordinates matter: game boards, matrices, image pixels, spreadsheets, fixed-size tables of numbers. It is the wrong tool when rows vary in length, when you insert and remove rows often, or when the elements are not primitives.
| Situation | Better fit | Why |
|---|---|---|
| Fixed rows and columns of numbers | int[][] | Fast, compact, indexes read like coordinates |
| Rows of different lengths | List<List<String>> | No manual row allocation, easy to grow |
| Rows of objects | List<Student> | One dimension of indexing is enough |
| Lookup by column name | Map | Coordinates are the wrong key |
The migration path is easy. Replace grid.length with grid.size(), grid[row].length with grid.get(row).size(), and the element type becomes a generic parameter. The loop nesting stays exactly the same, which is why most people can move a 2D array to a list of lists without relearning anything.
The rules worth remembering
- A 2D array is an array of array references; the two levels are allocated separately.
- Use
grid[row].lengthfor the inner bound, never the first row's length. - Allocate every row explicitly when you build a jagged array by hand.
- Two outer slots can point at one inner array, and writes then travel between them.
- Reach for a list of lists when rows vary, grow, or hold objects.
Try it yourself: build a jagged String[][] of three cinema rows with 4, 7 and 2 seats, mark three seats as booked by writing the string TAKEN into the cells, then print the row label, the seat numbers and the count of free seats. After that, deliberately read a cell that does not exist, catch the ArrayIndexOutOfBoundsException, and print the message so you can see exactly which index the runtime complains about.