Control Flow
Arrays
An array is a fixed-size block of same-typed slots, indexed from 0 to length-1. Its size is set at creation and never changes, and an out-of-range index throws at run time, not compile time. Covers creation and defaults, indexing, iteration, multidimensional and jagged arrays, and why a fixed array isn't a growable list. Every behavior is shown with verified output.
Suggest an editArrays — A Fixed Block of Slots
So far, each variable has held one value. An array holds many values of the same type in a single, fixed-size block: scores[0], scores[1], and so on. You reach each value by an integer index that counts from zero. Two properties define an array, and they cause every array bug:
- its size is chosen when you create it and never changes;
- indices run from
0tolength - 1, and stepping outside that range fails at run time, because the compiler can't know the index in advance.
Arrays are the raw, fast, fixed foundation. The growable collections in The Collections Framework are built on top of this idea.
💡 The core idea.
- An array holds many values of the same type in one fixed-size block, reached by an integer index from zero.
- Its size is chosen at creation and never changes.
- Indices run
0tolength - 1; stepping outside fails at run time.
This uses the loops that walk an array and the accumulation patterns that summarize one. Every output below was produced by compiling and running the code.
You'll be able to: create an array with new or a literal, and predict the default in each slot you did not fill; predict which index throws ArrayIndexOutOfBoundsException, and write the last index as a.length - 1; pick the classic or the enhanced for for a job, and walk an array backwards; walk a jagged 2D array using each row's own length; print, compare and grow an array with Arrays.toString, Arrays.equals and Arrays.copyOf.
📘 How to read the Intuition boxes. Each one is built in three moves:
- The mechanism — what the compiler and the JVM do.
- A concrete bite — a specific, runnable failure (often a real compiler error), shown so the trap is visible.
- The earned rule — the decision heuristic, now justified rather than asserted, plus its cost.
Table of contents
- Creating arrays
- Indexing: reading and writing slots
- Iterating an array
- Multidimensional and jagged arrays
- Arrays are not growable lists
- Mental-model summary
- Gotcha checklist
- Check yourself
- Sources
1. Creating arrays
There are two ways to make an array [1]:
new int[n]makes one withnslots, each set to a default (0forint);- an array literal
{…}makes one already filled.
Either way, .length reports its size.
Output:
3
0
4
2Analysis. new int[3] reserved three int slots, all 0. We set two of them, and scores[2], which we never touched, printed its default 0.
primes came pre-filled by its literal. scores.length is 3, and primes.length is 4. The diagram shows the shape: primes is one block of four numbered slots.
You've actually used an array since your very first program. main(String[] args) receives one, holding the words typed after java Main. The Run button types none, so it's empty:
Output:
0Intuition.
Mechanism. An array is one object holding length slots of its type, numbered from 0. new T[n] fills every slot with the type's default [2], while a literal fills it with what you wrote. How the slots sit in memory is the JVM's business, because the specification doesn't pin it down [3].
The default depends on the type:
Output:
0.0
false
nullA String[] slot starts as null: "no object yet". References, Equality & the Object Model covers null in full.
Concrete bite. The size is fixed at creation: .length is a property of the array, not a target you can change. (You'll feel that limit firsthand in §5.) For now, the point is that every slot exists from the start. An unassigned int slot is 0, not "empty" or "undefined".
Non-example: a literal assigned later. The {…} shorthand works only in a declaration [1]. Declare first and assign the braces later, and javac can't parse it:
Compiler error (the first of three errors on that line):
Main.java:4: error: illegal start of expression
a = {1, 2, 3};
^Name the type in front of the braces: a = new int[] {1, 2, 3}; compiles and prints 3. Don't give a size as well, though. javac rejects new int[3] {1, 2, 3} with array creation with both dimension expression and initialization is illegal.
Seeing an array. println on an array doesn't print its values. It prints a type code and a number:
Output (illustrative — the part after @ differs from run to run):
[I@2a139a55[I is Java's name for "array of int", and the hex digits come from the object's hash code [5]. To see the contents, pass the array to Arrays.toString, after import java.util.Arrays; [4]:
Output:
[5, 10, 15]
[0, 0, 0]The first loop assigned to a copy, as in Loops, so the array printed unchanged. The second wrote through the index, and every slot became 0. §3 comes back to that choice.
💡 Earned rule. Choose the form by what you know. Use a literal when you have the values up front. Use new T[n] when you'll fill the slots later, relying on the 0/false/null defaults until you do. Print an array with Arrays.toString.
The defaults have a cost: a slot you forgot to fill is silently 0, not an error. So a half-initialized array looks just like one full of 0s.
2. Indexing: reading and writing slots
array[i] is the slot at index i. Read it in an expression, or assign to it to change that slot. Indices start at 0, so a length-3 array has the valid indices 0, 1, 2.
Output:
10
99
30Analysis. a[1] = 99 overwrote the middle slot, and a[0] and a[2] were untouched. Reading and writing both use the same a[i] notation. On the left of =, it names the slot to change. Anywhere else, it yields the slot's value.
Intuition.
Mechanism. Every array access is checked at run time [6]. An index below 0, or at length or above, gets refused with an exception. Java never lets you read or write past the ends of an array.
Concrete bite. Index past the end (or below 0), and it throws. That happens at run time, because nobody knows the index until then:
Output (a thrown exception):
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: Index 3 out of bounds for length 3a has length 3, so the valid indices are 0, 1, 2, and a[3] is one past the end. The compiler accepted it (the index could have come from anywhere), and the JVM caught it when the line ran. The message names both the offending index and the length. a[-1] fails the same way, with Index -1 out of bounds for length 3.
💡 Earned rule. Index from 0 to length - 1. The last element is a[a.length - 1], never a[a.length].
The price of run-time checking is that you find an out-of-range index late: when that line runs, often with real data. So when the index comes from a calculation or input, check it against length before you use it.
3. Iterating an array
Two loops walk an array.
- The classic
forgives you the index, so you can reada[i], compare neighbours, or write back. - The enhanced
forgives you each value directly, for when you'd ignore the index anyway.
Output:
0 -> 5
1 -> 10
2 -> 15
5 10 15 Analysis. The first loop used i from 0 to nums.length - 1, printing each index alongside nums[i]. The second visited the values 5, 10, 15 with no index at all. Notice i < nums.length (not a hard-coded 3). Tie the boundary to the array, so it stays correct if the array's size changes.
Intuition.
Mechanism. The classic for reads nums[i] on each pass, using the index it controls. The enhanced for walks 0..length-1 for you and hands over each value [7]: a copy, as Loops showed. Same traversal, different access.
Concrete bite. The choice matters beyond style, because only the indexed for can write back. §1's Arrays.toString run showed it: for (int n : nums) n = 0; left [5, 10, 15], while the indexed loop gave [0, 0, 0].
Walking backwards needs the index too. Start at the last index, length - 1, and stop after 0: for (int i = a.length - 1; i >= 0; i--). Start at a.length, and it throws at once. Stop at i > 0, and you skip the first element.
💡 Earned rule. Use the enhanced for when you only read each value. Use the classic for when you need the index: to modify a slot, walk backwards, or compare an element to its neighbour.
Each choice has a cost. Reach for the index when you don't need it, and you bring back the off-by-one risk that the enhanced for removes. Use the enhanced for when you do need the index, and you can't write back.
4. Multidimensional and jagged arrays
An array's elements can themselves be arrays. int[][] is "an array of int arrays", a grid you address as grid[row][col]. Iterating over it takes nested loops.
Output:
2
3
6
21Analysis. grid.length is the number of rows (2). Each row is itself an array, so grid[0].length is that row's length (3), and grid[1][2] is the element at row 1, column 2 (6). The nested for-each walked every row, then every cell, summing to 21.
new int[rows][cols] makes a rectangle of defaults:
Output:
3
4
0Intuition. Mechanism. A 2D array is a one-dimensional array whose elements are other arrays. Nothing forces those inner arrays to share a length. A jagged array, where rows differ in size, is perfectly legal.
Concrete bite. So the bounds of one row tell you nothing about another, and if you assume a rectangle, it throws:
Output (two lengths, then a thrown exception):
4
1
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: Index 1 out of bounds for length 1Row 0 has four elements but row 1 has only one, so jagged[1][1] is out of bounds for that row. Each row carries its own length, and there's no single "column count" to trust.
💡 Earned rule. Treat a 2D array as rows that each have their own length. Iterate with grid[i].length per row, not one shared width. Reach for new int[rows][cols] when you want a rectangle.
Assume every row is the same length, and you pay with exactly this per-row ArrayIndexOutOfBoundsException. It shows up only when you index a short row past its end.
5. Arrays are not growable lists
Now the defining limit comes back: an array's length is fixed forever at creation. You can't append, insert, or remove. There's no add, and no length = …. To "grow" an array, you allocate a bigger one and copy. That rigidity is exactly what The Collections Framework replaces with growable Lists.
Compiler error:
Main.java:4: error: cannot find symbol
System.out.println(a.length());
^
symbol: method length()
location: variable a of type int[]
1 errorAnalysis. Writing a.length(), with parentheses as if it were a method, fails. An array's length is a field, written a.length with no parentheses. (Contrast a String, where s.length() is a method.) The slip is a compile error, so you catch it right away.
Intuition.
Mechanism. length is a final field fixed in the array at creation [8]. It isn't a method, and you can't reassign it. No operation changes an array's size, because the array was created with exactly that many slots.
Growing by copying. Arrays.copyOf(a, n) makes a new array of length n, copies a into it, and fills any extra slots with 0 [4]:
Output:
[1, 2, 3]
[1, 2, 3, 4, 0]a is unchanged. Each "add" to a full array costs a new array plus a copy of every element: O(N) work.
Non-example: = doesn't copy. Assigning one array variable to another shares the array. It doesn't make a second one:
Output:
99a and b name the same array, so a change through b shows through a. References, Equality & the Object Model explains why. For a real copy, use Arrays.copyOf(a, a.length).
The same sharing explains == on arrays. It asks "the same array?", not "the same contents?":
Output:
[1, 2, 3]
false
trueTwo arrays with equal contents are still two arrays. Arrays.equals compares them slot by slot.
💡 Earned rule. Use an array when the size is known and fixed. Reach for an ArrayList, in The Collections Framework, the moment the collection needs to grow or shrink. Copy with Arrays.copyOf, compare with Arrays.equals, and never with = or ==.
Force a growable problem onto an array, and you pay for allocate-and-copy over and over. Choose an array only when the size is truly fixed, and you pay for its fixed size just once, when you pick it.
6. Mental-model summary
| Principle | Consequence |
|---|---|
An array is a fixed-size block of same-typed slots, indexed from 0 |
Valid indices are 0..length-1; the last is a[a.length - 1] |
new T[n] fills slots with the type's default; a literal fills them |
An unassigned slot is silently 0/0.0/false/null, not "empty" |
A literal {…} works only in a declaration |
Assign later with new int[] {…} |
println(a) prints a type code and a hash, not the values |
Print with Arrays.toString(a) |
| Index bounds are checked at run time, not compile time | An out-of-range index throws ArrayIndexOutOfBoundsException when it runs |
| A 2D array is an array of arrays; rows may differ in length (jagged) | Each row has its own .length; there is no shared column count |
length is a fixed field, not a method, and never changes |
a.length (no parens); grow with Arrays.copyOf, or use a List |
= shares an array; == asks "the same array?" |
Copy with Arrays.copyOf; compare with Arrays.equals |
7. Gotcha checklist
| Symptom | Likely cause | Fix |
|---|---|---|
ArrayIndexOutOfBoundsException: Index 3 out of bounds for length 3 |
an index reached length (or went negative) |
the last valid index is length - 1; check computed indices |
| A backwards loop throws at once | it started at a.length |
start at a.length - 1 and run while i >= 0 |
A slot reads as 0 (or null) you thought you set |
new T[n] defaults; you didn't assign that slot |
assign it, or use a literal |
[I@2a139a55 printed instead of the values |
println on an array |
Arrays.toString(a) |
illegal start of expression at a = {1, 2, 3}; |
a literal assigned after the declaration | a = new int[] {1, 2, 3}; |
array creation with both dimension expression and initialization is illegal |
new int[3] {1, 2, 3} |
drop the size: new int[] {1, 2, 3} |
NegativeArraySizeException: -1 |
a negative size in new int[n] |
check n before creating the array |
| A for-each "edit" didn't stick | the loop variable is a copy | a classic for and a[i] = … |
cannot find symbol / method length() on an array |
length is a field |
a.length, without parentheses (only String uses s.length()) |
| A 2D index throws on some rows | the array is jagged | use each row's own grid[i].length |
| Changing one array changed "another" | b = a shares the array |
b = Arrays.copyOf(a, a.length) |
a == b is false for equal contents |
== compares identity |
Arrays.equals(a, b) |
| You need to add or remove elements | an array can't grow | Arrays.copyOf to a new length, or an ArrayList |
✅ Check yourself
One check per objective. Answer before you open anything.
The 🧪 box below: the length-4 array, the jagged sum, and the reversed print.
int[] a = new int[4]; a[0] = 5; a[3] = 9; gives a.length = 4 and a[1] = 0 (a default). a[4] throws ArrayIndexOutOfBoundsException: Index 4 out of bounds for length 4.
Summing { {1, 2}, {3, 4, 5} } with nested for-each gives 15. m[0][2] throws Index 2 out of bounds for length 2, because row 0 has only two slots.
Reversing needs the index, so use the classic for, and start at the last index, v.length - 1:
for (int i = v.length - 1; i >= 0; i--) { System.out.print(v[i] + " "); }
It prints 4 3 2 1.
📚 Sources
- The Java Language Specification, Java SE 25, §10.3 "Array Creation" and §10.6 "Array Initializers" — https://docs.oracle.com/javase/specs/jls/se25/html/jls-10.html#jls-10.6
- The Java Language Specification, Java SE 25, §4.12.5 "Initial Values of Variables" — https://docs.oracle.com/javase/specs/jls/se25/html/jls-4.html#jls-4.12.5
- The Java Virtual Machine Specification, Java SE 25, §2.7 "Representation of Objects" ("does not mandate any particular internal structure for objects") — https://docs.oracle.com/javase/specs/jvms/se25/html/jvms-2.html#jvms-2.7
- Java SE 25 API,
java.util.Arrays(toString,equals,copyOf) — https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/util/Arrays.html - Java SE 25 API,
java.lang.Object.toString("getClass().getName() + '@' + Integer.toHexString(hashCode())") — https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/lang/Object.html#toString() - The Java Language Specification, Java SE 25, §10.4 "Array Access" ("All array accesses are checked at run time") — https://docs.oracle.com/javase/specs/jls/se25/html/jls-10.html#jls-10.4
- The Java Language Specification, Java SE 25, §14.14.2 "The enhanced
forstatement" — https://docs.oracle.com/javase/specs/jls/se25/html/jls-14.html#jls-14.14.2 - The Java Language Specification, Java SE 25, §10.7 "Array Members" ("The
public finalfieldlength") — https://docs.oracle.com/javase/specs/jls/se25/html/jls-10.html#jls-10.7
🧪 Predict, then check.
- For
int[] a = new int[4];thena[0] = 5; a[3] = 9;, predicta.length,a[1], and whata[4]does. - Predict the output of summing
{ {1,2}, {3,4,5} }with nested for-each, and whatm[0][2]would do. - Write a loop that reverses the printing of
{1, 2, 3, 4}(last to first). Which loop shape do you need, and what is the starting index?
Your Turn
Before you move on, check your understanding with the coach — explain the idea, apply it, weigh the trade-offs, then defend your reasoning.