Array fundamentals
An array is a fixed-size block of equal-sized slots addressed by index. This lesson covers contiguous storage, zero-based indexing, capacity versus length, reading and writing, iteration, the real memory layout of a Java array and why cache locality makes arrays fast.
Learning goals
- Explain why array access is O(1)
- Track size separately from a fixed capacity
- Estimate the memory an array uses
- Explain why sequential array access is fast
Prerequisites
Contiguous indexed storage
An array stores its elements in one contiguous block of memory. Because every slot has the same size, element i lives at address base + i × slotSize: one multiplication and one addition, independent of the array's length. That is why reading a[i] is O(1).
Compare reading element k from an array with reaching element k in a linked list, which has to follow k references.
1int[] a = {10, 20, 30, 40};2int x = a[2]; // base + 2 * 4 bytes: one computed address, one read3// a linked list needs 2 hops to reach its third node
Reading element k: array vs linked list
Interactive · edit the inputarray memory (value, address)
The 7 values sit in consecutive memory cells, 4 bytes apart, starting at address 1000.
1static int getFromArray(int[] arr, int k) {2return arr[k]; // address = base + k * 4: one memory read3}45static int getFromList(Node head, int k) {6Node cur = head;7for (int i = 0; i < k; i++) {8cur = cur.next; // pointer chase: one read per hop9}10return cur.value;11}
Live state
k
5
base
1000
Execution trace (dry run table)Show 3 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L1 | k=5 base=1000 | The 7 values sit in consecutive memory cells, 4 bytes apart, starting at address 1000. |
| 2 | L2 | k=5 address=1020 | Address = 1000 + 5 × 4 = 1020. One multiplication and one addition give the location, no matter how large k is. |
| 3 | L2 | return=8 | return 8 with a single memory read: O(1) random access. |
Zero-based indexing
Java arrays are zero-based: the first element is a[0] and the last is a[a.length - 1]. Zero fits the address formula, since the offset of the first element is 0 slots from the base. Indexing outside 0 to length - 1 throws ArrayIndexOutOfBoundsException; Java checks every access.
Off-by-one errors almost always come from using <= where < is needed, or from forgetting that the last valid index is length - 1.
| Index | 0 | 1 | 2 | 3 | 4 |
|---|---|---|---|---|---|
| Offset from base (int) | 0 bytes | 4 bytes | 8 bytes | 12 bytes | 16 bytes |
1int[] a = new int[5];2for (int i = 0; i < a.length; i++) a[i] = i * i; // i runs 0..43// a[5] throws ArrayIndexOutOfBoundsException: Index 5 out of bounds for length 5
Capacity vs length
A Java array's length is its capacity: it is fixed when the array is created and never changes. A program often uses only part of the array, so it keeps a separate size variable for how many slots hold real data. Dynamic arrays such as ArrayList wrap exactly this pair.
In the animation, filled slots are the size and the empty slots are the spare capacity.
1int[] data = new int[8]; // capacity 82int size = 0; // nothing stored yet3data[size++] = 42; // size 14data[size++] = 7; // size 2: data.length is still 8
Dynamic array: capacity, growth and shrinking
Interactive · edit the inputsize elements in the buffer · length 0
data[] · capacity 1
A new dynamic array starts with capacity 1 and no elements.
1void add(int x) {2if (size == data.length) { // buffer is full3data = Arrays.copyOf(data, newCapacity()); // allocate bigger, copy size elements4}5data[size++] = x;6}7int removeLast() {8int x = data[--size];9if (size <= data.length / 4) data = Arrays.copyOf(data, Math.max(1, data.length / 2)); // optional shrink10return x;11}
Live state
size
0
capacity
1
Execution trace (dry run table)Show 21 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L1 | size=0 capacity=1 | A new dynamic array starts with capacity 1 and no elements. |
| 2 | L2 | size=0 capacity=1 x=4 | size 0 < capacity 1: there is a spare slot, so adding is O(1). |
| 3 | L5 | size=1 capacity=1 x=4 | Write 4 into data[0] and increase size to 1. |
| 4 | L2 | size=1 capacity=1 x=8 | size 1 equals capacity 1: no room for 8, so the buffer must grow. |
| 5 | L3 | size=1 capacity=2 | Allocate capacity 2 and copy the 1 element (1 copy so far). |
| 6 | L5 | size=2 capacity=2 x=8 | Write 8 into data[1] and increase size to 2. |
| 7 | L2 | size=2 capacity=2 x=15 | size 2 equals capacity 2: no room for 15, so the buffer must grow. |
| 8 | L3 | size=2 capacity=4 | Allocate capacity 4 and copy the 2 elements (3 copys so far). |
| 9 | L5 | size=3 capacity=4 x=15 | Write 15 into data[2] and increase size to 3. |
| 10 | L2 | size=3 capacity=4 x=16 | size 3 < capacity 4: there is a spare slot, so adding is O(1). |
| 11 | L5 | size=4 capacity=4 x=16 | Write 16 into data[3] and increase size to 4. |
| 12 | L2 | size=4 capacity=4 x=23 | size 4 equals capacity 4: no room for 23, so the buffer must grow. |
| 13 | L3 | size=4 capacity=8 | Allocate capacity 8 and copy the 4 elements (7 copys so far). |
| 14 | L5 | size=5 capacity=8 x=23 | Write 23 into data[4] and increase size to 5. |
| 15 | L2 | size=5 capacity=8 x=42 | size 5 < capacity 8: there is a spare slot, so adding is O(1). |
| 16 | L5 | size=6 capacity=8 x=42 | Write 42 into data[5] and increase size to 6. |
| 17 | L2 | size=6 capacity=8 x=7 | size 6 < capacity 8: there is a spare slot, so adding is O(1). |
| 18 | L5 | size=7 capacity=8 x=7 | Write 7 into data[6] and increase size to 7. |
| 19 | L2 | size=7 capacity=8 x=9 | size 7 < capacity 8: there is a spare slot, so adding is O(1). |
| 20 | L5 | size=8 capacity=8 x=9 | Write 9 into data[7] and increase size to 8. |
| 21 | L11 | size=8 capacity=8 copies=7 | Final: size 8, capacity 8 (0 spare slots), 3 resizes that copied 7 elements in total. Doubling keeps total copies below 2n. |
Read and write
Reading is x = a[i] and writing is a[i] = x. Both are O(1) and touch exactly one slot. For primitive arrays the value itself is stored in the slot; for object arrays the slot holds a reference, so writing replaces the reference, not the object.
Arrays are mutable and shared by reference: a method that receives an array can modify the caller's data.
1int[] a = {1, 2, 3};2a[1] = 20; // write: {1, 20, 3}3int y = a[1] + a[2]; // two reads: 234static void zeroFirst(int[] arr) { arr[0] = 0; } // the caller's array changes
Iteration
Visit every element with an indexed for loop when you need the index or want to write, with the enhanced for when you only read, and with Arrays.stream for pipelines. Iterating the whole array is O(n). Prefer iterating forward; it matches how memory is laid out.
Use the indexed form whenever you write to the array. The loop variable of an enhanced for is a copy.
1int[] a = {4, 8, 15};2for (int i = 0; i < a.length; i++) a[i] *= 2; // indexed: can write3int sum = 0;4for (int x : a) sum += x; // enhanced: read only (x is a copy)5int total = Arrays.stream(a).sum(); // stream: 8 + 16 + 30 = 54
Memory layout
On a 64-bit HotSpot JVM an array object has a 12-byte header plus a 4-byte length, so 16 bytes of overhead, followed by the elements, and the total is rounded up to a multiple of 8. An int[n] therefore needs about 16 + 4n bytes. An Integer[n] stores references (4 bytes each with compressed pointers) and each Integer is a separate 16-byte object.
Primitive arrays are much smaller and denser than arrays of wrappers.
| Array | Approximate size |
|---|---|
| int[10] | 16 + 40 = 56 bytes |
| long[10] | 16 + 80 = 96 bytes |
| boolean[10] | 16 + 10 = 26, rounded up to 32 bytes |
| int[1_000_000] | about 4 MB |
| Integer[1_000_000] (distinct values) | about 4 MB of references + 16 MB of objects |
Cache locality
Memory is loaded into the CPU cache in 64-byte lines, which hold 16 ints. Reading a[0] brings a[0..15] along, so the next 15 reads are cache hits. Sequential array traversal therefore runs close to memory bandwidth, while pointer-chasing structures miss the cache on nearly every step.
Run the simulator: row-order traversal of a 2D array reuses each loaded line, column order throws lines away.
Cache lines: row-major vs column-major traversal
Interactive · edit the inputmemory (row-major, 4 ints per cache line)
cache (2 lines, LRU)
A 4×8 matrix is stored row by row in one long run of memory. The CPU fetches memory in cache lines of 4 ints and keeps the 2 most recently used lines.
1int sumRowMajor(int[][] m) { // inner loop walks along a row2int sum = 0;3for (int r = 0; r < m.length; r++)4for (int c = 0; c < m[0].length; c++) sum += m[r][c];5return sum;6}7int sumColumnMajor(int[][] m) { // inner loop jumps down a column8int sum = 0;9for (int c = 0; c < m[0].length; c++)10for (int r = 0; r < m.length; r++) sum += m[r][c];11return sum;12}
Live state
rows
4
cols
8
lineSize
4
cache lines
2
Execution trace (dry run table)Show 34 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L3 | rows=4 cols=8 lineSize=4 cache lines=2 | A 4×8 matrix is stored row by row in one long run of memory. The CPU fetches memory in cache lines of 4 ints and keeps the 2 most recently used lines. |
| 2 | L4 | r=0 c=0 address=0 line=0 hits=0 misses=1 | m[0][0] is memory cell 0, on line 0, which is not cached: a miss. The CPU stalls while the whole line is loaded. |
| 3 | L4 | r=0 c=1 address=1 line=0 hits=1 misses=1 | m[0][1] is memory cell 1, on line 0, which is already in the cache: a hit (fast). |
| 4 | L4 | r=0 c=2 address=2 line=0 hits=2 misses=1 | m[0][2] is memory cell 2, on line 0, which is already in the cache: a hit (fast). |
| 5 | L4 | r=0 c=3 address=3 line=0 hits=3 misses=1 | m[0][3] is memory cell 3, on line 0, which is already in the cache: a hit (fast). |
| 6 | L4 | r=0 c=4 address=4 line=1 hits=3 misses=2 | m[0][4] is memory cell 4, on line 1, which is not cached: a miss. The CPU stalls while the whole line is loaded. |
| 7 | L4 | r=0 c=5 address=5 line=1 hits=4 misses=2 | m[0][5] is memory cell 5, on line 1, which is already in the cache: a hit (fast). |
| 8 | L4 | r=0 c=6 address=6 line=1 hits=5 misses=2 | m[0][6] is memory cell 6, on line 1, which is already in the cache: a hit (fast). |
| 9 | L4 | r=0 c=7 address=7 line=1 hits=6 misses=2 | m[0][7] is memory cell 7, on line 1, which is already in the cache: a hit (fast). |
| 10 | L4 | r=1 c=0 address=8 line=2 hits=6 misses=3 | m[1][0] is memory cell 8, on line 2, which is not cached: a miss. The CPU stalls while the whole line is loaded. |
| 11 | L4 | r=1 c=1 address=9 line=2 hits=7 misses=3 | m[1][1] is memory cell 9, on line 2, which is already in the cache: a hit (fast). |
| 12 | L4 | r=1 c=2 address=10 line=2 hits=8 misses=3 | m[1][2] is memory cell 10, on line 2, which is already in the cache: a hit (fast). |
| 13 | L4 | r=1 c=3 address=11 line=2 hits=9 misses=3 | m[1][3] is memory cell 11, on line 2, which is already in the cache: a hit (fast). |
| 14 | L4 | r=1 c=4 address=12 line=3 hits=9 misses=4 | m[1][4] is memory cell 12, on line 3, which is not cached: a miss. The CPU stalls while the whole line is loaded. |
| 15 | L4 | r=1 c=5 address=13 line=3 hits=10 misses=4 | m[1][5] is memory cell 13, on line 3, which is already in the cache: a hit (fast). |
| 16 | L4 | r=1 c=6 address=14 line=3 hits=11 misses=4 | m[1][6] is memory cell 14, on line 3, which is already in the cache: a hit (fast). |
| 17 | L4 | r=1 c=7 address=15 line=3 hits=12 misses=4 | m[1][7] is memory cell 15, on line 3, which is already in the cache: a hit (fast). |
| 18 | L4 | r=2 c=0 address=16 line=4 hits=12 misses=5 | m[2][0] is memory cell 16, on line 4, which is not cached: a miss. The CPU stalls while the whole line is loaded. |
| 19 | L4 | r=2 c=1 address=17 line=4 hits=13 misses=5 | m[2][1] is memory cell 17, on line 4, which is already in the cache: a hit (fast). |
| 20 | L4 | r=2 c=2 address=18 line=4 hits=14 misses=5 | m[2][2] is memory cell 18, on line 4, which is already in the cache: a hit (fast). |
| 21 | L4 | r=2 c=3 address=19 line=4 hits=15 misses=5 | m[2][3] is memory cell 19, on line 4, which is already in the cache: a hit (fast). |
| 22 | L4 | r=2 c=4 address=20 line=5 hits=15 misses=6 | m[2][4] is memory cell 20, on line 5, which is not cached: a miss. The CPU stalls while the whole line is loaded. |
| 23 | L4 | r=2 c=5 address=21 line=5 hits=16 misses=6 | m[2][5] is memory cell 21, on line 5, which is already in the cache: a hit (fast). |
| 24 | L4 | r=2 c=6 address=22 line=5 hits=17 misses=6 | m[2][6] is memory cell 22, on line 5, which is already in the cache: a hit (fast). |
| 25 | L4 | r=2 c=7 address=23 line=5 hits=18 misses=6 | m[2][7] is memory cell 23, on line 5, which is already in the cache: a hit (fast). |
| 26 | L4 | r=3 c=0 address=24 line=6 hits=18 misses=7 | m[3][0] is memory cell 24, on line 6, which is not cached: a miss. The CPU stalls while the whole line is loaded. |
| 27 | L4 | r=3 c=1 address=25 line=6 hits=19 misses=7 | m[3][1] is memory cell 25, on line 6, which is already in the cache: a hit (fast). |
| 28 | L4 | r=3 c=2 address=26 line=6 hits=20 misses=7 | m[3][2] is memory cell 26, on line 6, which is already in the cache: a hit (fast). |
| 29 | L4 | r=3 c=3 address=27 line=6 hits=21 misses=7 | m[3][3] is memory cell 27, on line 6, which is already in the cache: a hit (fast). |
| 30 | L4 | r=3 c=4 address=28 line=7 hits=21 misses=8 | m[3][4] is memory cell 28, on line 7, which is not cached: a miss. The CPU stalls while the whole line is loaded. |
| 31 | L4 | r=3 c=5 address=29 line=7 hits=22 misses=8 | m[3][5] is memory cell 29, on line 7, which is already in the cache: a hit (fast). |
| 32 | L4 | r=3 c=6 address=30 line=7 hits=23 misses=8 | m[3][6] is memory cell 30, on line 7, which is already in the cache: a hit (fast). |
| 33 | L4 | r=3 c=7 address=31 line=7 hits=24 misses=8 | m[3][7] is memory cell 31, on line 7, which is already in the cache: a hit (fast). |
| 34 | L5 | accesses=32 hits=24 misses=8 hit rate %=75 return=8 | 8 misss and 24 hits (75% hit rate). Both loops do identical arithmetic and are O(rows × cols), but walking along a row uses every int in a loaded line before moving on. Switch the order and compare. |
Practice ladder
Now try it yourself
Easy
- Compute the address offset of a[7] in an int[] and a long[]
- Why does a[a.length] throw?
Medium
- Estimate the memory of int[1_000_000] and Integer[1_000_000]
- Implement a tiny IntList with a fixed array and a size field
Advanced
- Explain why summing a 2D array row by row can be several times faster than column by column
Key takeaways
- Contiguous slots of equal size give O(1) indexed access.
- length is a fixed capacity; track the used size yourself or use ArrayList.
- Dense primitive arrays and sequential access are what CPU caches reward.