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

What this room checks you can do

Student can choose between int[] (or any T[]) and ArrayList<E> for a given problem by asking "is the size known up front?", and identify the specific cost of using ArrayList<Integer> for primitive workloads (autoboxing).

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.

What this room assumes you already have

Tasks

Do each one, then check the box. Checking a box is you saying you did it. You can uncheck a box if you check it by accident.

  1. trace
    Show the answer

    int[] of size n.

  2. trace
    Show the answer

    xs is [2, 3]; size() is 2.

  3. trace
    Show the answer

    error: "array required, but ArrayList<String> found."

  4. trace
    Show the answer

    ArrayList<String>.

Self check

Type what you think the answer is. Getting it wrong costs nothing and you can try as many times as you want.

Given the error "cannot find symbol: variable length" on a List<E> receiver, identify the fix.

Practice, untimed

Open this whenever you want, before the tasks or after them. Nothing in this section is recorded and nothing here is timed.

  1. Convert int[] xs = new int[10]; plus a counted-fill loop into the equivalent ArrayList<Integer> plus .add loop.write
    Show the answer

    ArrayList<Integer> xs = new ArrayList<>(); plus xs.add(sc.nextInt()) in the loop body.

  2. Given ArrayList<String> xs = new ArrayList<>(); xs.add("a"); xs.add("b"); xs.set(0, "X"); xs.add("c");, predict the final list.trace
    Show the answer

    ["X", "b", "c"].

  3. Given ArrayList<Integer> xs = new ArrayList<>(); xs.get(0);, predict the runtime outcome.trace
    Show the answer

    IndexOutOfBoundsException: Index 0 out of bounds for length 0.

  4. Convert a sequence of int[] operations (int[] xs = new int[3]; xs[0] = 10; xs[1] = 20; xs[2] = 30;) to the equivalent ArrayList<Integer> calls.write
    Show the answer

    add(10); add(20); add(30);.

How this room finishes

This room is done when all 4 tasks are checked and the self check is answered.