Room 10 of 14
· about 30 minutes
ArrayList, and when to use it instead of an array
Arrays are fixed at construction. This room is the type that is not, and the
rule for choosing between them.
Tasks checked0 of 4
XP earned on this path0
Room complete
Notes
When to use ArrayList<E> vs an array
ArrayList<E> is Java's standard resizable sequence type. An array is fixed-size at construction; an ArrayList grows and shrinks as the program runs. The choice between them is the choice between "I know the size upfront" and "I'll find out the size as I go."
// Array: size known at construction
final int[] scores = new int[students.length];
// ArrayList: grows as items are added
final ArrayList<String> visitedPages = new ArrayList<>();
while (sc.hasNextLine()) {
visitedPages.add(sc.nextLine()); // size grows by one each call
}
The choice is driven by when the size is determined. Three common cases:
1. Size known up front (use array): "read n test scores," "read 26 alphabet counts," "the user told me how many." int[] xs = new int[n]; and a counted loop are the right shape. 2. Size discovered while reading (use ArrayList): "read until end-of-file," "keep accumulating until the user says stop," "the predicate is dynamic." ArrayList.add saves the count-allocate-fill ceremony that reading a file of unknown length otherwise requires. 3. Size never changes but values do (use array): the lab-09 statistics methods all take int[] because the array's length is invariant; only the values change (or do not). Switching to ArrayList<Integer> would buy nothing and cost autoboxing.
Memory and performance
Behind the scenes, ArrayList<E> is an array. The class maintains an internal Object[] of "capacity," with a size field tracking how many slots are in use.
When add is called and the size equals the capacity, the class allocates a larger backing array (typically 1.5x the old capacity) and copies the existing elements. This makes add amortized O(1): most calls are cheap; occasional resize calls cost O(n), but they happen rarely enough to average out.
Random access (get(i), set(i, v)) is O(1) for both array and ArrayList. Linear search (indexOf) is O(n) for both. The performance gap shows up at the boundaries (adding past capacity, autoboxing for primitive types), not in the middle.
Why ArrayList<Integer> is not free for primitive workloads
ArrayList cannot hold primitives directly; only object types. To store int values in an ArrayList, each value must be autoboxed to Integer, costing one object allocation per insertion. For a 1-million-element accumulator, that is a million extra objects on the heap: much slower than an int[]. CSCD 210 labs that process primitive data use int[] (or double[]); labs that process objects can use either, but ArrayList<String> reads more naturally than String[] for variable-size data.
In other languages
- Python:
list is the only sequence type; it grows automatically; no separate "array" concept for general use. The array module exists for typed primitive arrays but is rarely used. - C++:
std::vector<T> is the ArrayList analog; T[] is the fixed-size array. Same trade-offs. - JavaScript:
Array is always resizable; no fixed-size variant in the language proper. TypedArray (Int32Array etc.) is the fixed-size, fixed-type variant. - Rust:
Vec<T> (resizable) vs [T; N] (fixed-size, type-level length). Same conceptual split as Java.
add, get, set, remove, size: the core ArrayList API
ArrayList<E> exposes its data through methods. The five methods CSCD 210 uses are:
final ArrayList<String> names = new ArrayList<>();
names.add("Ada"); // append at the end; size becomes 1
names.add("Bob"); // size = 2
names.add("Cay"); // size = 3
String first = names.get(0); // "Ada": random access by index
names.set(1, "Bjarne"); // overwrite slot 1; names is now ["Ada", "Bjarne", "Cay"]
names.remove(0); // remove slot 0; subsequent elements shift left
// names is now ["Bjarne", "Cay"]; size = 2
int n = names.size(); // 2
Each method does exactly one thing. The contract:
| Method | Returns | Effect on size | Complexity | |--------|---------|---------------|-----------:| | add(E elem) | boolean (always true) | size + 1 | amortized O(1) | | get(int i) | the element at i | unchanged | O(1) | | set(int i, E elem) | the previous element at i | unchanged | O(1) | | remove(int i) | the removed element | size - 1 | O(n): shifts everything after i left | | size() | int count of elements | unchanged | O(1) |
Indexing rules (same as arrays)
Valid indices for an ArrayList<E> of size n are 0, 1, …, n-1. get(n) throws IndexOutOfBoundsException. get(-1) throws too. The same rules as for an array's bracket-indexing: the storage model is the same, only the access syntax differs.
A subtle exception: add(int i, E elem) allows i from 0 to n inclusive. add(n, elem) is equivalent to add(elem): both append at the end. This is documented in the Javadoc but easy to forget.
Iterating
The enhanced-for works on ArrayList<E> exactly as it does on arrays:
for (String name : names) {
System.out.println(name);
}
The mechanics differ (the compiler desugars to an Iterator<E> call on a collection, not to an indexed counter on an array: JLS §14.14.2), but the syntax and visible behavior are identical.
What is NOT in CSCD 210 scope
ArrayList has many more methods (addAll, clear, contains, indexOf, subList, toArray, stream, forEach, sort). CSCD 210 stays with the five above. Generics beyond "put a type in the diamond" (ArrayList<String>) is also out of scope: ArrayList<? extends Animal> and friends are CSCD 211 territory.
In other languages
- Python:
list.append(v), lst[i], lst[i] = v, lst.pop(i) (or del lst[i]), len(lst). Same five operations, different names. - JavaScript:
arr.push(v), arr[i], arr[i] = v, arr.splice(i, 1), arr.length. Same. - C++:
vec.push_back(v), vec[i], vec[i] = v, vec.erase(vec.begin() + i), vec.size(). Same.
Array syntax does not work on ArrayList
ArrayList<E> looks like an array (it is named ArrayList, it has indexed access, it iterates like one), but the syntax to interact with it is completely method-based. Every operation that uses [] on an array uses a method call on an ArrayList:
| Array | ArrayList | |-------|-----------| | xs[i] (read) | xs.get(i) | | xs[i] = v (write) | xs.set(i, v) | | xs.length | xs.size() | | int[] xs = new int[5] | ArrayList<Integer> xs = new ArrayList<>() | | int[] xs = {1, 2, 3} | List<Integer> xs = List.of(1, 2, 3) (immutable) or new ArrayList<>(List.of(1, 2, 3)) (mutable) |
Mixing the two (writing xs[i] for an ArrayList or xs.get(i) for an array) does not compile. The compile errors are informative if you know what to look for:
ArrayList<Integer> xs = new ArrayList<>();
xs.add(1); xs.add(2); xs.add(3);
int v = xs[0]; // ERROR: array required, but ArrayList<Integer> found
xs[0] = 99; // ERROR: array required, but ArrayList<Integer> found
int n = xs.length; // ERROR: cannot find symbol: variable length
int[] arr = new int[]{1, 2, 3};
int v2 = arr.get(0); // ERROR: cannot find symbol: method get(int)
Each error message identifies the missing element. The fix is always to switch to the right syntax for the receiver's type, not to "make the syntax work somehow."
Why the language is set up this way
Java arrays predate generics by eight years. When ArrayList<E> was added (Java 1.2, 1998), it could not retrofit the [] operator onto user-defined types: Java has never supported operator overloading. The get/set method-based API was the only option that fit the language's design. C# took a different route (indexer properties, arr[i] works on List<T>); Java did not.
The asymmetry is a permanent feature, not a missing convenience. The CSCD 210 lab style works with it directly: students are expected to know both shapes and pick by the type.
Hover-the-type as the debugging tool
When the compiler reports "cannot find symbol: method get(int)" or "array required, but X found," the next step is to read the type of the variable. In IntelliJ or VS Code with the Java extension, hover over the variable name to see its declared type. If it is int[], switch to [i]. If it is ArrayList<...> or List<...>, switch to .get(i). The mistake is almost always picking the wrong syntax for the type; the fix is mechanical once the type is visible.
In other languages
- C#:
List<T> supports lst[i] syntax via the indexer property. Confusion does not arise. - Python:
list[i] works for both built-in lists and most array-like libraries; the unification is at the language level via __getitem__. - C++:
std::vector<T> supports vec[i] via operator[] overload. Same as C#. - Java: the only major language in this set where the user must know the type to pick the syntax. This is intentional historical design.