Demo: building MyArrayList and watching it resize
On This Lesson 11 topics
Lesson Overview
This is a hands-on lesson. You will build a small generic dynamic array, MyArrayList<T>, from nothing but an Object[] and a size counter. You will deliberately make the module's most common mistake, growing by one slot at a time, watch how many copies it causes, and then fix it with doubling and see the copies almost disappear.
Think of it as building the locker row from Lesson 1 with a moving service from Lesson 2 attached. By the end you will have the core of the first part of the lab, with every method's cost written beside it.
Why this matters: building the structure once removes the mystery from ArrayList. Whenever you later wonder what list.add or list.remove(0) costs, you can picture the code you wrote.
Follow along in any editor or IDE you like. Menu names differ between tools, so use whichever way your setup compiles and runs a single Java file, for example the javac and java commands in a terminal.
This lesson builds on every earlier lesson in the module and on generic classes (BEG-031).
Learning Objectives
By the end of this lesson you will be able to:
- Build
MyArrayList<T>withadd,get,remove(int), andsize. - Reject bad indexes with
IndexOutOfBoundsExceptionand a helpful message. - Observe the cost of +1 growth by counting resizes, and fix it with doubling.
- Shift items left on
removeand clear the freed slot. - Document the Big O of every method in a comment.
- Explain why the internal array is
Object[]and where the unchecked cast comes from.
Written Explanation
Step 1: the skeleton
Start with the two pieces of state and a small initial capacity, so that resizing happens quickly and is easy to watch.
import java.util.Arrays;
public class MyArrayList<T> {
private Object[] data = new Object[2]; // capacity 2
private int size; // items stored
static int resizes; // for the demo only
}The array is Object[], not T[]. Java does not let you write new T[...], because the type T is erased at runtime (the "trying new T()" family of problems from the generics module). The usual workaround is an Object[] and a cast when reading. The cast is safe here because only add(T) ever stores into the array. The compiler cannot prove it, so it reports an unchecked warning, and we silence that on the exact methods with @SuppressWarnings("unchecked").
Step 2: add and get
// O(1) amortized
public void add(T item) {
if (size == data.length) {
data = Arrays.copyOf(data, data.length + 1); // PLANNED MISTAKE
resizes++;
}
data[size++] = item;
}
// O(1)
@SuppressWarnings("unchecked")
public T get(int index) {
check(index);
return (T) data[index];
}
private void check(int index) {
if (index < 0 || index >= size)
throw new IndexOutOfBoundsException("index " + index + ", size " + size);
}Important: the bounds check uses size, not data.length. Slots between size and the capacity exist but are not part of the list, so reading them must be an error. get is O(1) because it is Lesson 1's array arithmetic plus a comparison.
Step 3: the planned mistake
Run 20 adds with the +1 rule from capacity 2:
MyArrayList<Integer> list = new MyArrayList<>();
for (int i = 0; i < 20; i++) list.add(i * 10);
System.out.println("size=" + list.size() + ", resizes=" + resizes);Real output with data.length + 1:
size=20, resizes=18Eighteen copies for twenty items. Every add after the second found the array full and copied everything. With 20 items that is not noticeable; with a million it would be catastrophic, because the total copying grows as n², as in Lesson 2.
Step 4: the fix, doubling
Change one expression:
data = Arrays.copyOf(data, data.length * 2);
System.out.println("resized to " + data.length);
resizes++;Real output for the same 20 adds:
resized to 4
resized to 8
resized to 16
resized to 32
size=20, resizes=4Four resizes instead of eighteen, at capacities 4, 8, 16, 32. Capacity 32 for 20 items is the cost of the space trade-off: twelve spare lockers. Also note why the copy count here is 4: the resizes happen when adding items 3, 5, 9, and 17.
Step 5: remove, size, and cleaning up
// O(n)
@SuppressWarnings("unchecked")
public T remove(int index) {
check(index);
T old = (T) data[index];
for (int i = index; i < size - 1; i++) {
data[i] = data[i + 1]; // shift left
}
data[--size] = null; // clear the freed slot
return old;
}
// O(1)
public int size() { return size; }Two details. First, shifting starts at index and moves forward, copying each item one place left; going backward would overwrite items you still need (Lesson 1, Mistake 3). Second, data[--size] = null does two jobs: it reduces size and clears the slot that is now spare. Without clearing, the array would still hold a reference to the last item, and the garbage collector could not reclaim it, a small memory leak. Using --size (decrement first) gives the index of the last stored item.
Step 6: write down the costs
| Method | Cost | Why |
|---|---|---|
add(item) | O(1) amortized, O(n) when it resizes | Write one slot; copy rarely |
get(index) | O(1) | Address arithmetic |
remove(index) | O(n) | Shifts up to n - 1 items |
size() | O(1) | Returns the counter |
In the lab you place these as comments above each method. Keep them honest: "amortized" is only for add, and the removal cost is O(n) in general, though removing the last item shifts nothing.
Examples
Example 1: the finished class (simple)
import java.util.Arrays;
public class MyArrayList<T> {
private Object[] data = new Object[2];
private int size;
// O(1) amortized
public void add(T item) {
if (size == data.length) {
data = Arrays.copyOf(data, data.length * 2);
}
data[size++] = item;
}
// O(1)
@SuppressWarnings("unchecked")
public T get(int index) { check(index); return (T) data[index]; }
// O(n)
@SuppressWarnings("unchecked")
public T remove(int index) {
check(index);
T old = (T) data[index];
for (int i = index; i < size - 1; i++) data[i] = data[i + 1];
data[--size] = null;
return old;
}
// O(1)
public int size() { return size; }
private void check(int index) {
if (index < 0 || index >= size)
throw new IndexOutOfBoundsException("index " + index + ", size " + size);
}
}This is the complete core. About twenty lines hold everything an ArrayList needs for these four operations.
Example 2: using it, including bad indexes (practical)
After adding 0, 10, ..., 190 (size 20), the demo ran:
System.out.println(list.remove(1) + " " + list.get(1) + " size=" + list.size());
list.get(19); // size is now 19, so valid indexes are 0..18
list.get(-1);Real output (the exceptions were caught and their messages printed):
10 20 size=19
index 19, size 19
index -1, size 19Removing index 1 returned 10, and 20 slid into its place. After the removal the size is 19, so index 19 is now invalid: the exception names both the index and the size, which makes the cause obvious in a log.
Example 3: adding in the middle and clearing (realistic)
Real lists also insert at a position. The same shifting idea as remove, in the other direction, with the same resize check:
// O(n)
void add(int index, T item) {
if (index < 0 || index > size)
throw new IndexOutOfBoundsException("index " + index + ", size " + size);
if (size == data.length) data = Arrays.copyOf(data, data.length * 2);
for (int i = size; i > index; i--) data[i] = data[i - 1];
data[index] = item;
size++;
}Note the check is index > size, not >=: inserting at size is allowed (it is an append). Adding "a", "c", "d" and then inserting "b" at index 1 gave abcd with size 4 and capacity 4 (real run). Trying add(9, ...) on a list of size 5 printed index 9, size 5. A clear() that nulls every slot and sets size = 0 leaves capacity untouched (size=0 cap=4), which shows again that capacity and size are different things.
Backend Insight
Building your own ArrayList is not something you will ship, but the habits are professional ones:
- Defensive checks with useful messages. A message like
index 19, size 19saves minutes in a log; "bad index" does not. - Writing the cost next to the code. Teams that document complexity catch O(n²) mistakes in review.
- Releasing references. The cleared slot is a small example of a bigger rule: long-lived structures (caches, pools, queues) must not keep references to objects they no longer need, or memory grows slowly until the service fails.
- Capacity as a tuning knob. Seeing the 32-slot array holding 20 items makes it obvious why
new ArrayList<>(expectedSize)exists.
Common Mistakes
Mistake 1: Growing by +1
- Incorrect:
data.length + 1: 18 copies for 20 items here, and O(n²) in general. - Correct:
data.length * 2, starting from a capacity greater than zero.
Mistake 2: Checking the index against data.length
get(5) on a list of size 3 and capacity 8 would silently return null. Check against size.
Mistake 3: Shifting in the wrong direction, or forgetting to clear
For remove, shift left from index. For add(index, ...), shift right from the last item. After a remove, null out the freed slot.
Mistake 4: Allowing index == size in get or remove
Valid indexes for reading are 0 to size - 1. Only insertion at index == size is allowed.
Exercises
Try each exercise yourself before opening the solutions.
Level A: Foundation
A1. MyArrayList starts with capacity 2 and doubles. What is the capacity after adding 5 items? After 9?
Show solution A1
A1. Capacity 2 holds 2 items; adding the 3rd doubles to 4; the 5th doubles to 8. So 5 items give capacity 8. The 9th item needs capacity 16, so 9 items give 16.
A2. A list has size 4 and capacity 8. Which indexes does get accept?
Show solution A2
A2. 0 to 3 (that is, size - 1). Slots 4 to 7 exist but are not part of the list.
A3. Why is the internal array an Object[] and not T[]?
Show solution A3
A3. Java does not allow creating generic arrays (new T[n]), because the type parameter is erased at runtime. An Object[] with a cast on reading is the standard workaround.
Level B: Application
B1. Write public boolean contains(Object o) for MyArrayList using equals. What is its cost?
Show solution B1
B1.
public boolean contains(Object o) {
for (int i = 0; i < size; i++) {
if (data[i].equals(o)) return true;
}
return false;
}O(n), a scan. (This version assumes no null items are stored.) It returned true for an existing item and false for a missing one (real run).
B2. Write public void clear() that empties the list so removed items can be collected. Should capacity change?
Show solution B2
B2.
public void clear() {
for (int i = 0; i < size; i++) data[i] = null;
size = 0;
}Capacity stays as it is: only size and the references change. The cost is O(n) because of the loop that clears the references. After clear on the list from Example 3, the run printed size=0 cap=4.
B3. The remove method has data[--size] = null. Trace it for a list of size 5 when you remove index 1: which items shift, what is size after, and which slot is cleared?
Show solution B3
B3. Items at indexes 2, 3, 4 move to 1, 2, 3 (three shifts). After the loop size goes from 5 to 4 and slot 4, which still holds the old last item, is set to null. The removed item was returned to the caller before shifting.
Level C: Challenge
C1. Write public void add(int index, T item) that inserts in the middle, with a bounds check that permits index == size. Which cases cost O(1)?
Show solution C1
C1. The code is the add(int index, T item) method from Example 3. Cases that cost O(1) (besides a possible resize) are index == size, an append, because the shifting loop runs zero times. Inserting at index 0 is the worst case: all items move. A real run gave abcd for the "a", "c", "d" plus "b" at index 1 test.
C2. Count the copies and total items copied for 20 adds from capacity 2 with doubling, and compare to the total for the +1 rule (18 copies).
Show solution C2
C2. Doubling from 2: resizes when adding items 3, 5, 9, 17: that is 4 copies, moving 2 + 4 + 8 + 16 = 30 items. The +1 rule: 18 copies, moving 2 + 3 + ... + 19 = 189 items (sum of 2 to 19: 19 * 20 / 2 - 1 = 189). The capacity after 20 adds with doubling is 32 (real run), against 20 for the +1 rule.
Mini Challenge
Extend the demo into a mini lab step. Add set(int index, T item) (replace and return the old value), isEmpty(), and a toString that prints the first size items as [a, b, c]. Then write a main that:
- Adds the numbers 1 to 10.
- Removes every second item by index (carefully: indexes shift after each removal).
- Prints the list and the number of resizes.
Check your work: the list shows only real items (no null from spare slots), set and get reject bad indexes with the same message style, and every method has a Big O comment above it.
Summary & Next Lesson
Key takeaways
MyArrayList<T>is anObject[]plussize; growth isArrays.copyOfinto a bigger array.- Growing by +1 gave 18 copies for 20 adds; doubling gave 4. The mistake is invisible in small tests and ruinous at scale.
- Bounds checks use
size, notdata.length. remove(index)shifts left, decrementssize, and clears the freed slot; it is O(n).- Write the cost of each method next to it:
addO(1) amortized,getO(1),removeO(n),sizeO(1).
Next lesson
This is the last lesson of the module. The module's lab, "Build your own", continues from here: it asks you to add MyLinkedList<T>, a stack, a queue, and a bracket-matching checker, each with its Big O documented.
Self-check
- What does
MyArrayListhold internally? - Why does
getcheck againstsize? - How many resizes do 20 adds cause with doubling from capacity 2?
- What does
data[--size] = nulldo? - Which
MyArrayListmethod is O(n) and why?
Show answers
- An
Object[]calleddataand anint size. - Slots at or beyond
sizeare spare, not part of the list, so reading them is an error. - Four: to capacities 4, 8, 16, and 32.
- It decrements
sizeand clears the now-spare last slot so the removed object is not kept alive. remove(int), because it shifts up to n - 1 items to close the gap.
References
- Oracle, Java SE 21 API, java.util.Arrays. Supports:
Arrays.copyOfreturns a new array of the requested length, padding with nulls for object arrays. - Oracle, Java SE 21 API, java.util.ArrayList. Supports: the real class's amortized constant-time
add, constant-timeget, and the capacity concept; the growth policy is not specified, so the doubling here is this demo's own choice. - All code, resize counts, and messages come from compiling and running the program on JDK 21. The explanation of why generic arrays cannot be created (type erasure) is standard Java behavior taught in the generics module and was not re-fetched from an official page for this lesson.
Practice
Apply this lesson with the module's lab, exercises and quiz.
Marking a lesson complete is optional and you can undo it any time. Your progress stays in this browser.