Kestrel Exams kestrel logo KESTREL EXAMSPRACTICE TO PASS
Java18 min readCode verified

2D arrays: rows, columns and the nested loop

A 2D array is a grid: rows across, columns down. This guide covers the [row][col] order, the two different lengths, walking the grid with nested loops in either direction, rows of different lengths, and arrays with three or more dimensions. Every listing was compiled and run on OpenJDK 21.

Practice this topic in the free app →All Java guides

A greenhouse with rows of pots#

Picture a small greenhouse with 3 benches, each holding 4 pots in a line. Every morning a sensor reads each pot’s soil moisture, from 0% (bone dry) to 100% (soaked). To name one pot you need two numbers: which bench, then which pot.

That is a two-dimensional array (2D array): a grid of values where each element is found with two indexes. The benches are the rows and the pot positions are the columns.

pot 0pot 1pot 2pot 3
bench 062485571
bench 135412938
bench 280778469

The rule for reaching one element is row first, then column: moisture[row][col]. So moisture[1][2] is bench 1, pot 2, which holds 29. If you use spreadsheets, note the switch: a spreadsheet cell such as C2 names the column first. In Java it is row first, always.

Trap check: where does counting start?

Both indexes start at 0, so the top-left reading is moisture[0][0]. With 3 benches of 4 pots, the bottom-right one is moisture[2][3], not moisture[3][4].

Declaring, creating and filling a grid#

You already know the 1D array from the arrays guide: int[] readings; is the type int plus one pair of square brackets. A 2D array uses the same shape with one change: a second pair of square brackets. int[][] moisture; reads as “moisture is an array of int arrays”.

The same three steps as before, one at a time:

StepCodeWhat it does
1. Declareint[][] moisture;makes a variable that can point at a grid of ints. No grid exists yet.
2. Createmoisture = new int[3][4];makes 3 rows of 4 columns, 12 elements. The row count comes first. Both numbers are counts, not last indexes.
3. Fillmoisture[1][2] = 29;puts a value in one element: row 1, column 2.
Try it first

Before you run it: after step 2, what is in each of the 12 elements? And in a String[][] made with new String[2][3]?

Show the answer

Zeros in the int grid and null in the String grid: default values, exactly like a 1D array. The output below confirms it.

Greenhouse.java
import java.util.Arrays;

public class Greenhouse {
    public static void main(String[] args) {
        int[][] moisture;                     // 1. declare
        moisture = new int[3][4];             // 2. create: 3 benches, 4 pots on each
        System.out.println(Arrays.deepToString(moisture));

        moisture[1][2] = 29;                  // 3. fill: bench 1, pot 2
        System.out.println(Arrays.deepToString(moisture));

        String[][] plant = new String[2][3];
        plant[0][1] = "basil";
        System.out.println(Arrays.deepToString(plant));
    }
}
[[0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]]
[[0, 0, 0, 0], [0, 0, 29, 0], [0, 0, 0, 0]]
[[null, basil, null], [null, null, null]]

Arrays.deepToString (from java.util.Arrays) prints a whole grid, with square brackets around the grid and around each row. It is the 2D partner of Arrays.toString.

Now the short form. If you already know the readings, an initializer list does all three steps at once. Nest one inner list per row, inside curly braces, and Java counts the rows and columns for you:

int[][] moisture = {
    {62, 48, 55, 71},
    {35, 41, 29, 38},
    {80, 77, 84, 69}
};

One row per line is a convention, not a rule; it makes the grid easy to check at a glance. As with 1D arrays, the bare curly-brace form only works on the declaration line. Elsewhere, write moisture = new int[][] {{62, 48}, {35, 41}};.

Two lengths, and why: an array of arrays#

A 1D array has one length. A grid has two sizes, rows and columns, and the way you ask for each is the most tested detail in this topic.

The reason is that Java has no true grid. A 2D array is an array whose elements are arrays. The outer array has one element per row, and each of those elements is a reference to a whole row: an ordinary 1D int[].

moisture ---> [ row 0 ] ---> { 62, 48, 55, 71 }
               [ row 1 ] ---> { 35, 41, 29, 38 }
               [ row 2 ] ---> { 80, 77, 84, 69 }

So moisture.length counts the elements of the outer array: the rows. moisture[1] on its own is row 1, a complete int[], and moisture[1][2] is element 2 of that row.

ExpressionMeansHere
moisture.lengthnumber of rows3
moisture[r].lengthnumber of columns in row r4
Try it first

The program below copies row 1 into a variable, int[] bench1 = moisture[1];, then sets bench1[2] = 60. Afterwards, what is moisture[1][2]: 29 or 60?

Show the answer

60. bench1 got a copy of the reference, not of the row. Rows are stored as references, just as the arrays guide showed for whole arrays, so both names reach the same four numbers. That is also why a single row can be passed to any method that takes an int[].

Greenhouse.java
import java.util.Arrays;

public class Greenhouse {
    public static void main(String[] args) {
        int[][] moisture = {
            {62, 48, 55, 71},                 // bench 0
            {35, 41, 29, 38},                 // bench 1
            {80, 77, 84, 69}                  // bench 2
        };
        System.out.println("Benches: " + moisture.length);
        System.out.println("Pots on bench 0: " + moisture[0].length);

        int[] bench1 = moisture[1];           // one whole row, an ordinary int[]
        System.out.println(Arrays.toString(bench1));
        bench1[2] = 60;                       // water pot 2 through the row variable
        System.out.println(moisture[1][2]);

        System.out.println(Arrays.toString(moisture));
        System.out.println("Pot 3 on bench 1? " + moisture[3][1]);
    }
}
Benches: 3
Pots on bench 0: 4
[35, 41, 29, 38]
60
[[I@7ea987ac, [I@12a3a380, [I@29453f44]
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: Index 3 out of bounds for length 3
	at Greenhouse.main(Greenhouse.java:19)

The [[I@... line is the strange output, shown on purpose: Arrays.toString goes only one level deep, so it prints the three row references. [I means “array of int”, and the part after @ is a hash code that will be different when you run it. Use Arrays.deepToString to see the values.

The last line swaps the indexes for “pot 3 on bench 1”: moisture[3][1]. It compiles, then throws ArrayIndexOutOfBoundsException. Index 3 against length 3 tells you the row index was too big.

Trap check: row or column first?

Every time you write two indexes, say them in your head: “row, then column”. On any grid, swapped indexes read the wrong element. On a grid that is not square, they can also crash, as here.

Visiting every pot with nested loops#

To visit every element you need a nested loop: one loop inside another. The outer loop picks a row; the inner loop runs through that whole row before the outer loop moves on. We call the loop variables r (row, bench) and c (column, pot); you will often see those names on exams.

Greenhouse.java
public class Greenhouse {
    public static void main(String[] args) {
        int[][] moisture = {
            {62, 48, 55, 71},
            {35, 41, 29, 38},
            {80, 77, 84, 69}
        };
        for (int r = 0; r < moisture.length; r++) {             // each bench (row)
            String line = "Bench " + r + ":";
            for (int c = 0; c < moisture[r].length; c++) {      // each pot (column) on it
                line += " " + moisture[r][c];
            }
            System.out.println(line);
        }

        String walk = "Down the pots:";
        for (int c = 0; c < moisture[0].length; c++) {          // each pot position
            for (int r = 0; r < moisture.length; r++) {         // each bench
                walk += " " + moisture[r][c];
            }
        }
        System.out.println(walk);
    }
}
Bench 0: 62 48 55 71
Bench 1: 35 41 29 38
Bench 2: 80 77 84 69
Down the pots: 62 35 80 48 41 77 55 29 84 71 38 69

The first pair of loops is row-major order: outer loop over rows, inner over columns. It reads across each bench, like a page, and is the usual order. The second pair is column-major order: outer loop over columns, inner over rows, reading down each pot position.

Predict before you check: column-major starts at 62, goes down to 35 and 80, and only then moves on to pot 1 (48). The output starts 62 35 80 48, one column of the top table at a time, so it makes sense.

Either way the element is written moisture[r][c], row first; only the loop order changed. The column-major version’s moisture[0].length assumes every bench has the same number of pots.

Trap check: < or <=?

Both loops use <: r < moisture.length and c < moisture[r].length. The last valid index is always length - 1.

Shrink it to the smallest case. On one bench of one pot, {{50}}, the outer loop runs with r = 0 and the inner with c = 0. Then the inner test 1 < 1 is false, so the body runs exactly once. With <=, it would run again with c = 1 and throw. With zero benches, new int[0][4], the outer test 0 < 0 fails at once and nothing runs, which is also correct.

Row totals, column totals and the driest pot#

Most 2D exam questions use one of three patterns: total each row, total each column, or find the biggest or smallest element. The totals use the accumulator pattern: start at 0, add one value per pass, read it at the end. The new question is where the accumulator starts.

Here is a first attempt at the average moisture of each bench. Before you look at the output, decide what range an average can possibly be in.

Greenhouse.java
public class Greenhouse {
    public static void main(String[] args) {
        int[][] moisture = {
            {62, 48, 55, 71},
            {35, 41, 29, 38},
            {80, 77, 84, 69}
        };
        int benchTotal = 0;                   // starts once, before the outer loop
        for (int r = 0; r < moisture.length; r++) {
            for (int c = 0; c < moisture[r].length; c++) {
                benchTotal += moisture[r][c];
            }
            double average = (double) benchTotal / moisture[r].length;
            System.out.println("Bench " + r + " average: " + average + "%");
        }
    }
}
Bench 0 average: 59.0%
Bench 1 average: 94.75%
Bench 2 average: 172.25%

Does this make sense? Bench 2’s average is 172.25%, but no reading can go above 100. Not even the soggiest pot is 172% water. Bench 1’s 94.75 is higher than any pot on bench 1, which is just as impossible. An average must sit between its smallest and largest value, so the code is wrong even though it ran. That five-second check matters just as much when an AI tool hands you the answer.

The bug: benchTotal starts at 0 once, before the outer loop, so each bench’s total includes every bench before it. The fix moves one line: reset the accumulator at the start of each row, inside the outer loop and before the inner one.

Greenhouse.java
public class Greenhouse {
    public static void main(String[] args) {
        int[][] moisture = {
            {62, 48, 55, 71},
            {35, 41, 29, 38},
            {80, 77, 84, 69}
        };
        for (int r = 0; r < moisture.length; r++) {
            int benchTotal = 0;               // reset at the start of every bench
            for (int c = 0; c < moisture[r].length; c++) {
                benchTotal += moisture[r][c];
            }
            double average = (double) benchTotal / moisture[r].length;
            System.out.println("Bench " + r + " total " + benchTotal + ", average " + average + "%");
        }

        int driestR = 0;
        int driestC = 0;
        for (int r = 0; r < moisture.length; r++) {
            for (int c = 0; c < moisture[r].length; c++) {
                if (moisture[r][c] < moisture[driestR][driestC]) {
                    driestR = r;
                    driestC = c;
                }
            }
        }
        System.out.println("Driest: " + moisture[driestR][driestC] + "% at bench " + driestR + ", pot " + driestC);
    }
}
Bench 0 total 236, average 59.0%
Bench 1 total 143, average 35.75%
Bench 2 total 310, average 77.5%
Driest: 29% at bench 1, pot 2
  • Row totals: outer loop over rows, accumulator reset inside it, inner loop over that row’s columns.
  • Column totals: the same with the loops swapped: outer loop over columns, reset inside it, inner loop over rows. You will write it yourself below.
  • Smallest element: assume moisture[0][0] is driest and let the loops prove otherwise. Remember the position (driestR, driestC), not just the value, which needs counting loops.
  • Grand total: when positions do not matter, nested for-each loops work too: for (int[] bench : moisture), then for (int reading : bench).
Type this one yourself

Do not paste it. Write a method potTotals(int[][] grid) that returns an int[] holding the total of each column: each pot position, added across all the benches. Typing the loops is how the order of r and c sticks. Assume the grid is rectangular. Test it on the readings above and print the result with Arrays.toString.

Show the answer

The result needs one slot per column, so its size is grid[0].length. Each slot starts at 0, so it is already a ready-made accumulator:

static int[] potTotals(int[][] grid) {
    int[] totals = new int[grid[0].length];           // one total per column
    for (int c = 0; c < grid[0].length; c++) {
        for (int r = 0; r < grid.length; r++) {
            totals[c] += grid[r][c];
        }
    }
    return totals;
}
[177, 166, 168, 178]
Trap check: integer or floating-point division?

Every average is a division, so ask what type each side is. Both benchTotal and moisture[r].length are int, so without the cast bench 1 would be 143 / 4, which is 35. The cast (double) benchTotal converts the total to a double first, so 35.75 keeps its fraction.

Benches of different lengths: ragged arrays#

The greenhouse gets a 2-pot corner bench and a 3-pot middle bench. A grid whose rows differ in length is a ragged (or jagged) array. It works because each row is a separate 1D array.

Build it in steps: create the outer array with only the row count, new int[3][], then create each row with its own size. Only the later sizes may be left out, so new int[][4] does not compile.

Greenhouse.java
import java.util.Arrays;

public class Greenhouse {
    public static void main(String[] args) {
        int[][] moisture = new int[3][];      // 3 benches, no rows made yet
        System.out.println(Arrays.deepToString(moisture));

        moisture[0] = new int[] {62, 48, 55, 71};
        moisture[1] = new int[] {35, 41};     // the short corner bench
        moisture[2] = new int[] {80, 77, 84};
        System.out.println(Arrays.deepToString(moisture));

        for (int r = 0; r < moisture.length; r++) {
            String line = "Bench " + r + " (" + moisture[r].length + " pots):";
            for (int c = 0; c < moisture[r].length; c++) {
                line += " " + moisture[r][c];
            }
            System.out.println(line);
        }
    }
}
[null, null, null]
[[62, 48, 55, 71], [35, 41], [80, 77, 84]]
Bench 0 (4 pots): 62 48 55 71
Bench 1 (2 pots): 35 41
Bench 2 (3 pots): 80 77 84

Two things to notice:

  • Right after new int[3][], the grid prints [null, null, null]: the outer array exists, but no rows yet. Touching moisture[0][0] then would throw a NullPointerException.
  • The loop is safe because its inner loop uses moisture[r].length, the length of this row. On a ragged grid, moisture[0].length would be wrong for the other rows.

An initializer list works too: int[][] moisture = {{62, 48, 55, 71}, {35, 41}, {80, 77, 84}}; builds the same grid.

Passing a grid to a method, and returning one#

A grid parameter has the type int[][], and the method receives a copy of the reference, so it works on the caller’s grid. A method can also build a new array and return it, as rowAverages does below with one average per row.

Greenhouse.java
import java.util.Arrays;

public class Greenhouse {
    static double[] rowAverages(int[][] grid) {
        double[] averages = new double[grid.length];      // one slot per row
        for (int r = 0; r < grid.length; r++) {
            int total = 0;
            for (int c = 0; c < grid[r].length; c++) {
                total += grid[r][c];
            }
            averages[r] = (double) total / grid[r].length;
        }
        return averages;
    }

    public static void main(String[] args) {
        int[][] moisture = {
            {62, 48, 55, 71},
            {35, 41, 29, 38},
            {80, 77, 84, 69}
        };
        System.out.println(Arrays.toString(rowAverages(moisture)));

        int[][] minutesLate = {
            {4, 0, 5},
            {1, 3}
        };
        System.out.println(Arrays.toString(rowAverages(minutesLate)));
    }
}
[59.0, 35.75, 77.5]
[3.0, 2.0]

rowAverages returns a double[] with one slot per row. Its inner loop uses grid[r].length, so ragged grids are safe too.

Look at the last line. minutesLate has nothing to do with plants: it holds how late a bus was on each trip, one row per route. rowAverages handles it unchanged, because nothing inside it is about greenhouses. That is why it has general names, and why one method with a parameter serves any grid: a gradebook, a timetable, a game board.

Three dimensions and beyond#

Suppose the business grows to 2 greenhouses of 3 benches with 4 pots each. A three-dimensional (3D) array is an array of 2D arrays: site[house][bench][pot]. Each extra dimension adds one more pair of square brackets, one more size after new and one more nested loop.

Try it first

The program below numbers every pot 0, 1, 2 and so on in the order the three loops visit them. What number lands in site[1][2][3]?

Show the answer

23. There are 2 × 3 × 4 = 24 pots, numbered 0 to 23. site[1][2][3] is the last pot on the last bench of the last house, so it is visited last.

Greenhouse.java
public class Greenhouse {
    public static void main(String[] args) {
        int[][][] site = new int[2][3][4];    // 2 houses, 3 benches each, 4 pots per bench
        int count = 0;
        for (int h = 0; h < site.length; h++) {
            for (int r = 0; r < site[h].length; r++) {
                for (int c = 0; c < site[h][r].length; c++) {
                    site[h][r][c] = count;    // number each pot in the order it is visited
                    count++;
                }
            }
        }
        System.out.println("Pots: " + count);
        System.out.println(site.length + " x " + site[0].length + " x " + site[0][0].length);
        System.out.println("House 1, bench 2, pot 3: " + site[1][2][3]);
    }
}
Pots: 24
2 x 3 x 4
House 1, bench 2, pot 3: 23

Each .length reads one level: site.length is 2 (houses), site[0].length is 3 (benches) and site[0][0].length is 4 (pots). The element count is their product, 24. Arrays with four or more dimensions are legal but rare.

What was new in this guide#

  1. int[][]: the 1D type plus a second pair of square brackets.
  2. new int[3][4] and nested initializer lists, row count first.
  3. moisture[r][c]: row first, then column, both from 0.
  4. moisture.length counts rows; moisture[r].length counts that row’s columns.
  5. Each row is an ordinary int[], stored as a shared reference.
  6. Nested loops in row-major or column-major order.
  7. Row totals, column totals and the smallest-element search.
  8. Ragged arrays: new int[3][], then each row separately.
  9. Passing a grid to a method, and returning an array.
  10. 3D arrays, and Arrays.deepToString to print any grid.
One sentence to carry into the exam

A 2D array is an array of rows: grid[r][c] is row first, grid.length counts rows, and grid[r].length counts the columns in row r. Outer loop over rows reads across, and outer loop over columns reads down. A row total is reset inside the outer loop, before the inner one.

Where these go wrong#

  • Swapping the indexes. It is grid[row][col]. On any grid, swapped indexes read the wrong element. On a grid that is not square, they can also crash.
  • Using the wrong length. grid.length is the number of rows; columns are grid[r].length. On a ragged array, grid[0].length is wrong for the other rows, so use grid[r].length in the inner loop.
  • Starting the row total outside the outer loop. Each row’s total then includes every row before it. Reset it inside the outer loop.
  • Dividing two ints for an average. 143 / 4 is 35. Cast one side to double first.
  • Printing a 2D array with Arrays.toString. You get row references such as [I@ and a hash code. Use Arrays.deepToString.
  • Writing new int[][4]. The row count must come first; only the later sizes may be left out.
  • Reading a row of new int[3][] before creating it. The row is null until you make it, so the read throws NullPointerException.

On the exam: if a question says “write a method”, write only the method: its header, its body and its return. If it says “write a program”, include the class and main. On 2D questions, partial credit usually lives in the loop headers. Write those carefully: the right length in each one, and < rather than <=. Read the question, read it again, answer it, then read it once more to check you did every part.

Practice#

Answer each one on paper before you look at the solutions. Every answer that involves running code was checked by compiling and running it.

  1. For int[][] g = new int[4][6];, what are g.length, g[0].length, the number of elements, and the index of the bottom-right element?
  2. What prints?
    int[][] m = new int[3][2];
    for (int r = 0; r < m.length; r++) {
        for (int c = 0; c < m[r].length; c++) {
            m[r][c] = r * 10 + c;
        }
    }
    System.out.println(m.length + " " + m[0].length);
    System.out.println(m[2][1]);
    System.out.println(m[1][0] + m[0][1]);
  3. Write a method that returns the sum of the main diagonal of a square 2D array: the elements where the row index equals the column index.
  4. Given a grid with 3 rows and 5 columns, how many times does the inner loop body run in a full row-major traversal? In a full column-major one?
  5. Write a method that counts the zeros in a 2D array whose rows may be different lengths.
  6. How many elements does new double[2][5][10] make, and how do you read the size of the last dimension?

Solutions#

  1. g.length is 4 (rows), g[0].length is 6 (columns), there are 24 elements, and the bottom-right element is g[3][5].
  2. Each element is ten times its row plus its column, so m[2][1] is 21, and m[1][0] + m[0][1] is 10 + 1:
    public class QTrace {
        public static void main(String[] args) {
            int[][] m = new int[3][2];
            for (int r = 0; r < m.length; r++) {
                for (int c = 0; c < m[r].length; c++) {
                    m[r][c] = r * 10 + c;
                }
            }
            System.out.println(m.length + " " + m[0].length);
            System.out.println(m[2][1]);
            System.out.println(m[1][0] + m[0][1]);
        }
    }
    3 2
    21
    11
  3. Only one loop is needed, because row and column move together:
    public class QDiag {
        public static int diagonalSum(int[][] sq) {
            int sum = 0;
            for (int i = 0; i < sq.length; i++) {
                sum += sq[i][i];
            }
            return sum;
        }
    
        public static void main(String[] args) {
            int[][] sq = {
                {2, 0, 1},
                {5, 3, 9},
                {4, 6, 7}
            };
            System.out.println(diagonalSum(sq));
        }
    }
    12
    For the grid shown, the diagonal is 2 + 3 + 7 = 12.
  4. 15 both times: every element is visited exactly once. Row-major runs the outer loop 3 times with 5 inner passes each; column-major runs the outer loop 5 times with 3 inner passes each.
  5. Nested for-each loops never need either length, so they are safe on a ragged array:
    public class QCount {
        public static int countZeros(int[][] g) {
            int zeros = 0;
            for (int[] row : g) {
                for (int x : row) {
                    if (x == 0) {
                        zeros++;
                    }
                }
            }
            return zeros;
        }
    
        public static void main(String[] args) {
            int[][] g = {
                {0, 3},
                {1, 0, 0},
                {7}
            };
            System.out.println(countZeros(g));
        }
    }
    3
  6. 2 × 5 × 10 = 100 elements, all starting at 0.0. The last dimension’s size is arr[0][0].length, which is 10.

Test-readiness checklist#

These are the items from the course’s Exam 2 topic list that this guide covers. Tick each one only when you can do it without looking.

  • ☐  I can create and initialize a two-dimensional array and reach any element with [row][col].
  • ☐  I can tell grid.length from grid[r].length and explain why a 2D array is an array of arrays.
  • ☐  I can traverse a 2D array row-major and column-major, and compute row and column totals.
  • ☐  I can work with ragged arrays and multidimensional arrays of three or more dimensions.

Test yourself in the free Kestrel Exams app

40 practice questions on 2D, ragged and multidimensional arrays, with every code answer checked by compiling and running it. Free, offline, no account.

Practise 2D arrays →

Frequently asked questions#

How do you declare a 2D array in Java?

Use two pairs of square brackets: int[][] grid = new int[3][4]; creates 3 rows of 4 columns, every element starting at 0. To fill it at once, nest initializer lists, one inner list per row: int[][] grid = {{1, 2}, {3, 4}};.

Which index comes first in a 2D array, row or column?

The row. grid[r][c] means row r, column c, and both indexes start at 0.

How do you get the number of rows and columns of a 2D array?

grid.length is the number of rows. grid[r].length is the number of columns in row r. On a rectangular array every row has the same length, so grid[0].length gives the column count.

What is a ragged (jagged) array?

A 2D array whose rows have different lengths. It works because a 2D array is an array of separate 1D row arrays. Create it with only the row count, as in new int[4][], then create each row separately.

What is the difference between row-major and column-major traversal?

Row-major order uses the outer loop for rows and the inner loop for columns, so it reads across each row. Column-major order swaps the loops and reads down each column. The element is written grid[r][c] either way.

How do you print a 2D array in Java?

Use Arrays.deepToString(grid) from java.util.Arrays, or nested loops. Arrays.toString on a 2D array prints the references of the row arrays rather than their values.

Suggest a change

Something here not clear? A topic you wish we covered? Tell us. We read every message, and a request is the fastest way to get a guide written — several of these exist because somebody asked.