When I first met arrays in Java, the basics felt easy: put some values in a row, pick one by its number. Then came int[][], and my brain got stuck. Is it a table? A grid? Why can some rows be shorter than others? And why did my for-each loop refuse to change my values?

All of those questions have one answer: you need to know how Java actually stores arrays. Once you see that, 1D arrays, 2D arrays, jagged arrays and for-each all start to feel like one idea instead of four.

Unless I show a full class, the snippets below go inside a main method. Some of them use Arrays, so add import java.util.Arrays; at the top of your file.

What is an array?

An array is a fixed-size box of slots. Every slot holds the same type of value, and each slot has a number called an index. Indexes start at 0, not 1.

int[] scores = {80, 92, 75}; System.out.println(scores[1]); System.out.println(scores.length); 

Output:

92 3 

Two things to notice:

  • scores[1] is the second value, because counting starts at 0.
  • scores.length has no brackets. It's a field, not a method. This trips up many beginners, because strings use length() and lists use size().

Three ways to create an array

int[] a = new int[3]; // 3 slots, filled with default values int[] b = {10, 20, 30}; // shortcut when you know the values int[] c = new int[]{10, 20, 30}; // same thing, written longer 

When you use new int[3] without values, Java fills every slot with a default:

  • 0 for numbers
  • false for booleans
  • null for objects like String
int[] marks = new int[3]; String[] names = new String[2]; System.out.println(marks[0]); System.out.println(names[0]); 

Output:

0 null 

What happens behind the scenes

An array is an object. When you write new int[3], the JVM creates it on the heap, remembers its length, and gives you a reference (an address) that you store in your variable.

 STACK HEAP +---------+ +-----------------+ | scores -|--------> | length = 3 | +---------+ | [0] [1] [2] | | 80 92 75 | +-----------------+ 

Because the JVM knows the length, it checks every index you use. If you go too far, you don't corrupt memory like you might in C. You get a clear error instead:

int[] scores = {80, 92, 75}; System.out.println(scores[3]); 

Output:

Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: Index 3 out of bounds for length 3 

The valid indexes here are 0, 1 and 2. The last valid index is always length - 1.

Looping through an array

int[] scores = {80, 92, 75}; int total = 0; for (int i = 0; i < scores.length; i++) { total += scores[i]; } System.out.println("Total: " + total); 

Output:

Total: 247 

Notice i < scores.length and not <=. Using <= would try to read scores[3] and crash.

Two array facts that surprise people

1. Printing an array directly gives gibberish.

int[] nums = {1, 2, 3}; System.out.println(nums); System.out.println(Arrays.toString(nums)); 

Output (the first line will look a bit different on your machine):

[I@6d06d69c [1, 2, 3] 

The first line is the type ([I means "array of int") plus a code for the object. Use Arrays.toString() to see the real values.

2. Assigning an array copies the reference, not the array.

int[] a = {1, 2, 3}; int[] b = a; b[0] = 99; System.out.println(a[0]); 

Output:

99 

Both a and b point to the same array on the heap. If you want a real copy, use Arrays.copyOf(a, a.length) or a.clone().

2D arrays: an array of arrays

Now the part that confused me. A 2D array looks like a table with rows and columns:

int[][] grid = { {1, 2, 3}, {4, 5, 6} }; System.out.println(grid.length); System.out.println(grid[0].length); System.out.println(grid[1][2]); 

Output:

2 3 6 
  • grid.length is the number of rows (2).
  • grid[0].length is the number of columns in row 0 (3).
  • grid[1][2] means row 1, column 2, which is 6.

The big secret: Java has no real "2D" array

In Java, int[][] is an array whose slots each hold a reference to another array. In some other languages a 2D array is one flat block of memory. In Java it's arrays inside an array.

 grid +--------+ | ref 0 -|-----> [ 1 | 2 | 3 ] | ref 1 -|-----> [ 4 | 5 | 6 ] +--------+ (outer array) (row arrays, each its own object) 

So grid[0] is not a number. It's a whole int[], and grid[0][1] just means "take row 0, then take slot 1 of that row."

Once you see this, grid.length and grid[0].length make perfect sense. They're just the length of two different arrays.

Looping through a 2D array

You need two loops: the outer one walks the rows, the inner one walks the columns of the current row.

int[][] grid = { {1, 2, 3}, {4, 5, 6} }; for (int i = 0; i < grid.length; i++) { for (int j = 0; j < grid[i].length; j++) { System.out.print(grid[i][j] + " "); } System.out.println(); } 

Output:

1 2 3 4 5 6 

Look at the inner loop's limit: grid[i].length, not grid[0].length. Using the current row's length is safer, and it's what makes the next topic possible.

Creating a 2D array with a size

int[][] table = new int[2][3]; // 2 rows, 3 columns, all zeros table[1][2] = 7; System.out.println(Arrays.deepToString(table)); 

Output:

[[0, 0, 0], [0, 0, 7]] 

For 2D arrays, Arrays.toString() would show the row addresses, so use Arrays.deepToString() instead.

Jagged arrays: rows of different lengths

Since each row is its own array, nothing forces the rows to be the same size. An array whose rows have different lengths is called a jagged array.

Here's one that builds a small triangle:

int[][] triangle = new int[3][]; // 3 rows, row sizes not set yet for (int i = 0; i < triangle.length; i++) { triangle[i] = new int[i + 1]; // row i gets i+1 slots for (int j = 0; j < triangle[i].length; j++) { triangle[i][j] = j + 1; } } for (int[] row : triangle) { System.out.println(Arrays.toString(row)); } 

Output:

[1] [1, 2] [1, 2, 3] 

Look at the first line: new int[3][]. We gave the number of rows but left the column count empty. Java creates the outer array and fills it with null. We then create each row ourselves, with whatever size we like.

 triangle +--------+ | ref 0 -|-----> [ 1 ] | ref 1 -|-----> [ 1 | 2 ] | ref 2 -|-----> [ 1 | 2 | 3 ] +--------+ 

You can also write a jagged array directly:

int[][] days = { {5, 6}, // week 1: only 2 entries {7, 8, 9, 10}, // week 2: 4 entries {3} // week 3: 1 entry }; 

Why use it? When your data isn't a neat rectangle. Think of the number of subjects each student takes, or the number of orders per customer. A normal 2D array would waste memory on empty slots. A jagged array uses exactly what you need.

The one trap: if you skip creating a row and then use it, you hit null.

int[][] x = new int[2][]; x[0][0] = 5; // NullPointerException, row 0 was never created 

The for-each loop (and its variable)

The enhanced for loop, also called for-each, is the tidy way to visit every item without caring about indexes.

int[] scores = {80, 92, 75}; for (int s : scores) { System.out.println(s); } 

Output:

80 92 75 

Read the : as "in": "for each s in scores". The variable s is called the loop variable, and it's the part people misunderstand.

The loop variable is a copy

On each round, Java copies the current value into s. It does not give you the slot itself. So changing s does nothing to the array:

int[] nums = {1, 2, 3}; for (int n : nums) { n = n * 10; } System.out.println(Arrays.toString(nums)); 

Output:

[1, 2, 3] 

The array didn't change, because we only changed the copy. To really change the values, use an indexed loop:

for (int i = 0; i < nums.length; i++) { nums[i] = nums[i] * 10; } System.out.println(Arrays.toString(nums)); 

Output:

[10, 20, 30] 

What about arrays of objects?

For objects, the copy is a copy of the reference. So you can change what the object contains, but you can't swap the object in the array:

StringBuilder[] items = { new StringBuilder("a"), new StringBuilder("b") }; for (StringBuilder sb : items) { sb.append("!"); // changes the real object } for (StringBuilder sb : items) { sb = new StringBuilder("x"); // only re-points the copy, no effect } System.out.println(items[0] + " " + items[1]); 

Output:

a! b! 

For-each on a 2D array

Remember that a 2D array is an array of arrays. So the outer loop variable is a whole row, and you need a second loop for the values inside it:

int[][] grid = { {1, 2, 3}, {4, 5, 6} }; int sum = 0; for (int[] row : grid) { for (int value : row) { sum += value; } } System.out.println(sum); 

Output:

21 

Notice the outer variable's type is int[], not int. This is the "array of arrays" idea showing up again, and it works the same for jagged arrays, since each row brings its own length.

What happens behind the scenes

For arrays, the compiler quietly rewrites your for-each into a normal indexed loop. Your loop:

for (int n : nums) { // use n } 

becomes roughly:

int[] copyOfRef = nums; int len = copyOfRef.length; for (int i = 0; i < len; i++) { int n = copyOfRef[i]; // use n } 

That explains everything: n is a fresh variable filled from the slot on each round, so changing it can't touch the array. It also explains why for-each has no index. The index exists, but the compiler hides it from you.

Which one should I use?

  • Array or ArrayList? Use an array when the size is fixed and known. Use an ArrayList when the size changes while the program runs, because arrays can't grow.
  • 2D array or jagged array? Use a regular 2D array for rectangular data like a chessboard, a seat map or an image. Use a jagged array when rows naturally have different lengths.
  • for or for-each? Use for-each when you only need to read every item. Use a normal for when you need the index, want to go backwards or skip items, or need to change the values in the array.
  • Printing: use Arrays.toString() for 1D and Arrays.deepToString() for 2D.

Common mistakes

  1. Using <= instead of < in the loop. i <= arr.length goes one step too far and throws ArrayIndexOutOfBoundsException. The last valid index is length - 1.
  2. Printing an array directly. You get [I@6d06d69c instead of your values. Use Arrays.toString().
  3. Thinking b = a copies the array. It copies only the reference. Both names control one array. Use Arrays.copyOf() or clone() for a real copy.
  4. Comparing arrays with ==. It checks whether they're the same object, not whether the values match. Use Arrays.equals() (or Arrays.deepEquals() for 2D).
  5. Using a jagged row before creating it. new int[3][] leaves every row as null, so touching one gives a NullPointerException.
  6. Expecting for-each to change the array. The loop variable is a copy. Use an indexed loop when you need to write values back.

Quick recap

  • An array is a fixed-size row of same-type slots, stored as an object on the heap, with indexes from 0.
  • Use length (no brackets) for arrays. The JVM checks every index and throws a clear error if you go out of range.
  • A 2D array is really an array of arrays: the outer array holds references to the row arrays.
  • A jagged array is a 2D array whose rows have different lengths. Rows you haven't created yet are null.
  • For-each gives you a copy of each item, and the compiler turns it into a normal indexed loop behind the scenes.
  • For a 2D array, the outer for-each variable is a whole row (int[]), not a single number.

Try it yourself

Here's a small practice idea: store the marks of three students, where each student took a different number of subjects. Use a jagged array, then use a nested for-each to print each student's average. After that, try changing a value inside a for-each and see what happens. Then fix it with an indexed loop.

If you want to go one step further, write your own printGrid method that works for any 2D or jagged array. It's a great way to prove to yourself that you've understood "array of arrays."

If this helped, or if you spotted something I should fix, tell me in the comments. I'm still learning too.