Before we touch syntax, students need a mental model. An array is a single object that holds a fixed-size sequence of values of the same type, stored in a contiguous block of memory and accessed by integer position (called an index). Java picked up zero-based indexing from C and C++; the first slot is index 0, the last is index length - 1. Java also adds something C did not: every access is bounds-checked at runtime. The history of why bounds checking exists (Morris worm, Code Red, SQL Slammer) is the price-of-admission story for understanding why ArrayIndexOutOfBoundsException is a feature, not a nuisance.
Fixed-size homogeneous sequence
Student can state that a Java array is a single object holding a fixed-size sequence of same-typed values, and explain what each adjective rules out.
An array is a single object that holds a fixed-size sequence of values of the same type. Two adjectives carry the weight: fixed-size means once you have constructed the array you cannot grow or shrink it (to "grow" you build a bigger one and copy), and homogeneous means every element has the same declared element type: int[] holds only int values, String[] holds only String references. The array itself is one object on the heap; the variable that names it holds a reference to that object.
This single-object property is why an array can be passed to a method, returned from a method, stored as a field, and aliased by another variable. The slots inside it are not separate variables: they are positions inside one object. That is the mental shift from loops, where every counter or accumulator was its own named variable.
final int[] temps = new int[5]; // one int[] object holding 5 int slots
temps[0] = 72; // fill the first slot
temps[1] = 68; // ... and so on
In other languages
- Python:
listis the closest analog but is dynamic (it can grow); the truly fixed-size typed counterpart isarray.array('i', [0]*5)ornumpy.zeros(5, dtype=int). - C:
int temps[5];is a fixed-size sequence on the stack; there is no "array object": just a contiguous run of bytes named by the identifier. - Kotlin:
IntArray(5)is the homogeneous primitive-int counterpart;Array<Int>boxes each element.
Bounds and indices from zero
Student can compute the legal index range of an array of length n and write expressions for the first, last, and middle elements.
Java numbers array slots starting at 0. An array of length n has legal indices 0, 1, ..., n - 1. The first element is at index 0; the last element is at index n - 1, never n. The expression arr.length gives n, so arr[arr.length - 1] is the last element and arr[arr.length] is always out of bounds.
Zero-based indexing is inherited from C (where the index is literally an offset added to a base address). Java keeps the C numbering but adds a runtime guard: any attempt to use an index outside 0..length-1 throws ArrayIndexOutOfBoundsException instead of silently reading or writing neighboring memory. This is the most useful runtime check in CS1, because the off-by-one error it catches is the most common bug in array code.
final int[] xs = {10, 20, 30, 40, 50}; // length 5
System.out.println(xs[0]); // 10 (first)
System.out.println(xs[xs.length - 1]); // 50 (last)
// System.out.println(xs[xs.length]); // ArrayIndexOutOfBoundsException
In other languages
- Python: also zero-based;
xs[-1]is a Python convenience for "last element" that Java does not provide. - C: zero-based, but no bounds check:
xs[xs_len]reads whatever byte happens to live just past the array. This is the source of the buffer-overrun bugs that Java's bounds check prevents. - Kotlin: zero-based; throws
ArrayIndexOutOfBoundsExceptionlike Java.
Buffer overrun history (and why Java bounds-checks)
Student can explain why Java throws ArrayIndexOutOfBoundsException and name at least one historical worm enabled by C's lack of bounds checking.
Why does Java throw ArrayIndexOutOfBoundsException instead of just returning whatever lives at the bad address? Because the languages that do not throw an exception: C and C++: produced a generation of catastrophic security bugs called buffer overruns (or buffer overflows). Writing past the end of an array in C silently overwrites adjacent memory; if an attacker controls the data being written, they can overwrite a return address or a function pointer and take over the program. The 1988 Morris worm (the first internet worm), the 2001 Code Red worm, and the 2003 SQL Slammer worm all exploited this exact class of bug. Java was designed in the early 1990s with this history in mind, and bounds-checking arrays was a deliberate language-design decision: every read and write of arr[i] is preceded by a runtime check that 0 <= i < arr.length. C# (Microsoft, 2000) made the same choice for the same reason.
The message for CS1 is short. The exception is a gift. It tells you exactly where the bug is. In C, the same bug would silently corrupt your program and possibly create a security hole. When you see ArrayIndexOutOfBoundsException, you are seeing the runtime save you from a Morris-worm-class bug.
final int[] xs = new int[5];
xs[10] = 99; // Java: ArrayIndexOutOfBoundsException at runtime.
// C equivalent: silently writes into whatever memory follows xs.
In other languages
- C: no bounds check.
xs[10] = 99;writes into whatever memory follows the array: sometimes a local variable, sometimes a return address. This is the bug that Morris/Code Red/SQL Slammer exploited. - Python: bounds-checked; raises
IndexError. Same philosophy as Java. - Rust: bounds-checked at runtime by default; the compiler can elide the check when bounds are statically provable.