Chapter 8: Arrays and Dynamic Arrays
8.01Beginner / Intermediate7 topics14 min3 interactive animations

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

Java prerequisitesComplexity basics
8.01.1

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.

Java
1int[] a = {10, 20, 30, 40};
2int x = a[2]; // base + 2 * 4 bytes: one computed address, one read
3// a linked list needs 2 hops to reach its third node

Reading element k: array vs linked list

Interactive · edit the input

Your input

Try

array memory (value, address)

12@1000
7@1004
31@1008
4@1012
19@1016
8@1020
25@1024
Setup

The 7 values sit in consecutive memory cells, 4 bytes apart, starting at address 1000.

Step 1 / 3
Java
1static int getFromArray(int[] arr, int k) {
2 return arr[k]; // address = base + k * 4: one memory read
3}
4
5static int getFromList(Node head, int k) {
6 Node cur = head;
7 for (int i = 0; i < k; i++) {
8 cur = cur.next; // pointer chase: one read per hop
9 }
10 return cur.value;
11}

Live state

0 memory reads

k

5

base

1000

No condition evaluated on this step
Execution trace (dry run table)Show 3 steps
StepLineStateWhat happens
1L1k=5 base=1000The 7 values sit in consecutive memory cells, 4 bytes apart, starting at address 1000.
2L2k=5 address=1020Address = 1000 + 5 × 4 = 1020. One multiplication and one addition give the location, no matter how large k is.
3L2return=8return 8 with a single memory read: O(1) random access.
8.01.2

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.

Index01234
Offset from base (int)0 bytes4 bytes8 bytes12 bytes16 bytes
Java
1int[] a = new int[5];
2for (int i = 0; i < a.length; i++) a[i] = i * i; // i runs 0..4
3// a[5] throws ArrayIndexOutOfBoundsException: Index 5 out of bounds for length 5
8.01.3

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.

Java
1int[] data = new int[8]; // capacity 8
2int size = 0; // nothing stored yet
3data[size++] = 42; // size 1
4data[size++] = 7; // size 2: data.length is still 8

Dynamic array: capacity, growth and shrinking

Interactive · edit the input

Your input

Try

size elements in the buffer · length 0

Used slot (size)Spare capacity

data[] · capacity 1

0
Setup

A new dynamic array starts with capacity 1 and no elements.

Step 1 / 21
Java
1void add(int x) {
2 if (size == data.length) { // buffer is full
3 data = Arrays.copyOf(data, newCapacity()); // allocate bigger, copy size elements
4 }
5 data[size++] = x;
6}
7int removeLast() {
8 int x = data[--size];
9 if (size <= data.length / 4) data = Arrays.copyOf(data, Math.max(1, data.length / 2)); // optional shrink
10 return x;
11}

Live state

0 copies0 resizes

size

0

capacity

1

No condition evaluated on this step
Execution trace (dry run table)Show 21 steps
StepLineStateWhat happens
1L1size=0 capacity=1A new dynamic array starts with capacity 1 and no elements.
2L2size=0 capacity=1 x=4size 0 < capacity 1: there is a spare slot, so adding is O(1).
3L5size=1 capacity=1 x=4Write 4 into data[0] and increase size to 1.
4L2size=1 capacity=1 x=8size 1 equals capacity 1: no room for 8, so the buffer must grow.
5L3size=1 capacity=2Allocate capacity 2 and copy the 1 element (1 copy so far).
6L5size=2 capacity=2 x=8Write 8 into data[1] and increase size to 2.
7L2size=2 capacity=2 x=15size 2 equals capacity 2: no room for 15, so the buffer must grow.
8L3size=2 capacity=4Allocate capacity 4 and copy the 2 elements (3 copys so far).
9L5size=3 capacity=4 x=15Write 15 into data[2] and increase size to 3.
10L2size=3 capacity=4 x=16size 3 < capacity 4: there is a spare slot, so adding is O(1).
11L5size=4 capacity=4 x=16Write 16 into data[3] and increase size to 4.
12L2size=4 capacity=4 x=23size 4 equals capacity 4: no room for 23, so the buffer must grow.
13L3size=4 capacity=8Allocate capacity 8 and copy the 4 elements (7 copys so far).
14L5size=5 capacity=8 x=23Write 23 into data[4] and increase size to 5.
15L2size=5 capacity=8 x=42size 5 < capacity 8: there is a spare slot, so adding is O(1).
16L5size=6 capacity=8 x=42Write 42 into data[5] and increase size to 6.
17L2size=6 capacity=8 x=7size 6 < capacity 8: there is a spare slot, so adding is O(1).
18L5size=7 capacity=8 x=7Write 7 into data[6] and increase size to 7.
19L2size=7 capacity=8 x=9size 7 < capacity 8: there is a spare slot, so adding is O(1).
20L5size=8 capacity=8 x=9Write 9 into data[7] and increase size to 8.
21L11size=8 capacity=8 copies=7Final: size 8, capacity 8 (0 spare slots), 3 resizes that copied 7 elements in total. Doubling keeps total copies below 2n.
8.01.4

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.

Java
1int[] a = {1, 2, 3};
2a[1] = 20; // write: {1, 20, 3}
3int y = a[1] + a[2]; // two reads: 23
4static void zeroFirst(int[] arr) { arr[0] = 0; } // the caller's array changes
8.01.5

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.

Java
1int[] a = {4, 8, 15};
2for (int i = 0; i < a.length; i++) a[i] *= 2; // indexed: can write
3int 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
8.01.6

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.

ArrayApproximate 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
8.01.7

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 input

Your input

Try

memory (row-major, 4 ints per cache line)

0line 0
1line 0
2line 0
3line 0
4line 1
5line 1
6line 1
7line 1
8line 2
9line 2
10line 2
11line 2
12line 3
13line 3
14line 3
15line 3
16line 4
17line 4
18line 4
19line 4
20line 5
21line 5
22line 5
23line 5
24line 6
25line 6
26line 6
27line 6
28line 7
29line 7
30line 7
31line 7

cache (2 lines, LRU)

empty
Setup

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.

Step 1 / 34
Java
1int sumRowMajor(int[][] m) { // inner loop walks along a row
2 int sum = 0;
3 for (int r = 0; r < m.length; r++)
4 for (int c = 0; c < m[0].length; c++) sum += m[r][c];
5 return sum;
6}
7int sumColumnMajor(int[][] m) { // inner loop jumps down a column
8 int sum = 0;
9 for (int c = 0; c < m[0].length; c++)
10 for (int r = 0; r < m.length; r++) sum += m[r][c];
11 return sum;
12}

Live state

0 hits0 misses

rows

4

cols

8

lineSize

4

cache lines

2

No condition evaluated on this step
Execution trace (dry run table)Show 34 steps
StepLineStateWhat happens
1L3rows=4 cols=8 lineSize=4 cache lines=2A 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.
2L4r=0 c=0 address=0 line=0 hits=0 misses=1m[0][0] is memory cell 0, on line 0, which is not cached: a miss. The CPU stalls while the whole line is loaded.
3L4r=0 c=1 address=1 line=0 hits=1 misses=1m[0][1] is memory cell 1, on line 0, which is already in the cache: a hit (fast).
4L4r=0 c=2 address=2 line=0 hits=2 misses=1m[0][2] is memory cell 2, on line 0, which is already in the cache: a hit (fast).
5L4r=0 c=3 address=3 line=0 hits=3 misses=1m[0][3] is memory cell 3, on line 0, which is already in the cache: a hit (fast).
6L4r=0 c=4 address=4 line=1 hits=3 misses=2m[0][4] is memory cell 4, on line 1, which is not cached: a miss. The CPU stalls while the whole line is loaded.
7L4r=0 c=5 address=5 line=1 hits=4 misses=2m[0][5] is memory cell 5, on line 1, which is already in the cache: a hit (fast).
8L4r=0 c=6 address=6 line=1 hits=5 misses=2m[0][6] is memory cell 6, on line 1, which is already in the cache: a hit (fast).
9L4r=0 c=7 address=7 line=1 hits=6 misses=2m[0][7] is memory cell 7, on line 1, which is already in the cache: a hit (fast).
10L4r=1 c=0 address=8 line=2 hits=6 misses=3m[1][0] is memory cell 8, on line 2, which is not cached: a miss. The CPU stalls while the whole line is loaded.
11L4r=1 c=1 address=9 line=2 hits=7 misses=3m[1][1] is memory cell 9, on line 2, which is already in the cache: a hit (fast).
12L4r=1 c=2 address=10 line=2 hits=8 misses=3m[1][2] is memory cell 10, on line 2, which is already in the cache: a hit (fast).
13L4r=1 c=3 address=11 line=2 hits=9 misses=3m[1][3] is memory cell 11, on line 2, which is already in the cache: a hit (fast).
14L4r=1 c=4 address=12 line=3 hits=9 misses=4m[1][4] is memory cell 12, on line 3, which is not cached: a miss. The CPU stalls while the whole line is loaded.
15L4r=1 c=5 address=13 line=3 hits=10 misses=4m[1][5] is memory cell 13, on line 3, which is already in the cache: a hit (fast).
16L4r=1 c=6 address=14 line=3 hits=11 misses=4m[1][6] is memory cell 14, on line 3, which is already in the cache: a hit (fast).
17L4r=1 c=7 address=15 line=3 hits=12 misses=4m[1][7] is memory cell 15, on line 3, which is already in the cache: a hit (fast).
18L4r=2 c=0 address=16 line=4 hits=12 misses=5m[2][0] is memory cell 16, on line 4, which is not cached: a miss. The CPU stalls while the whole line is loaded.
19L4r=2 c=1 address=17 line=4 hits=13 misses=5m[2][1] is memory cell 17, on line 4, which is already in the cache: a hit (fast).
20L4r=2 c=2 address=18 line=4 hits=14 misses=5m[2][2] is memory cell 18, on line 4, which is already in the cache: a hit (fast).
21L4r=2 c=3 address=19 line=4 hits=15 misses=5m[2][3] is memory cell 19, on line 4, which is already in the cache: a hit (fast).
22L4r=2 c=4 address=20 line=5 hits=15 misses=6m[2][4] is memory cell 20, on line 5, which is not cached: a miss. The CPU stalls while the whole line is loaded.
23L4r=2 c=5 address=21 line=5 hits=16 misses=6m[2][5] is memory cell 21, on line 5, which is already in the cache: a hit (fast).
24L4r=2 c=6 address=22 line=5 hits=17 misses=6m[2][6] is memory cell 22, on line 5, which is already in the cache: a hit (fast).
25L4r=2 c=7 address=23 line=5 hits=18 misses=6m[2][7] is memory cell 23, on line 5, which is already in the cache: a hit (fast).
26L4r=3 c=0 address=24 line=6 hits=18 misses=7m[3][0] is memory cell 24, on line 6, which is not cached: a miss. The CPU stalls while the whole line is loaded.
27L4r=3 c=1 address=25 line=6 hits=19 misses=7m[3][1] is memory cell 25, on line 6, which is already in the cache: a hit (fast).
28L4r=3 c=2 address=26 line=6 hits=20 misses=7m[3][2] is memory cell 26, on line 6, which is already in the cache: a hit (fast).
29L4r=3 c=3 address=27 line=6 hits=21 misses=7m[3][3] is memory cell 27, on line 6, which is already in the cache: a hit (fast).
30L4r=3 c=4 address=28 line=7 hits=21 misses=8m[3][4] is memory cell 28, on line 7, which is not cached: a miss. The CPU stalls while the whole line is loaded.
31L4r=3 c=5 address=29 line=7 hits=22 misses=8m[3][5] is memory cell 29, on line 7, which is already in the cache: a hit (fast).
32L4r=3 c=6 address=30 line=7 hits=23 misses=8m[3][6] is memory cell 30, on line 7, which is already in the cache: a hit (fast).
33L4r=3 c=7 address=31 line=7 hits=24 misses=8m[3][7] is memory cell 31, on line 7, which is already in the cache: a hit (fast).
34L5accesses=32 hits=24 misses=8 hit rate %=75 return=88 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.