ArrayList — Dynamic Lists for Test Data

8 min read

Arrays from chapter 3 are fixed-size: declare String[5] and you have five slots, no more, no fewer. Real test data is rarely that disciplined. You don't know in advance how many results a search will return, how many users a fixture will have, or how many failures a test run will produce. ArrayList is Java's dynamic-array — the same idea as a JavaScript array, with add, remove, and a size() that updates as the list grows. It's the single most-used container in modern Java code, and it's the workhorse of every test data layer you'll write.

Importing and creating

ArrayList lives in java.util. Import it once at the top of the file:

import java.util.ArrayList;
import java.util.List;

Creating one:

ArrayList<String> browsers = new ArrayList<>();

The <String> is a generic type parameter — it tells the compiler "this list holds Strings, nothing else." ArrayList<Integer>, ArrayList<TestUser>, ArrayList<long[]> — pick any type. The empty <> after new is the "diamond operator" — Java infers the same type as the variable's declaration. (You can write new ArrayList<String>() too; it's just verbose.)

The everyday operations

import java.util.ArrayList;
import java.util.List;
 
public class BrowserList {
    public static void main(String[] args) {
        ArrayList<String> browsers = new ArrayList<>();
 
        browsers.add("Chrome");                // append
        browsers.add("Firefox");
        browsers.add("Safari");
        browsers.add(0, "Edge");               // insert at index 0
 
        System.out.println(browsers);          // toString built in — readable
 
        System.out.println("size: " + browsers.size());
        System.out.println("first: " + browsers.get(0));
        System.out.println("last:  " + browsers.get(browsers.size() - 1));
 
        browsers.set(2, "Brave");              // replace at index 2
        browsers.remove("Safari");             // remove by value
        browsers.remove(0);                    // remove by index
 
        System.out.println("contains Firefox? " + browsers.contains("Firefox"));
        System.out.println("indexOf Brave:    " + browsers.indexOf("Brave"));
        System.out.println("final list:       " + browsers);
    }
}

Output:

[Edge, Chrome, Firefox, Safari]
size: 4
first: Edge
last:  Safari
[Chrome, Firefox, Brave]
contains Firefox? true
indexOf Brave:    2
final list:       [Chrome, Firefox, Brave]

Compare to arrays:

OperationArrayArrayList
Add at endn/a (fixed size)list.add(x)
Insert in middlen/alist.add(i, x)
Removen/alist.remove(i) or list.remove(value)
Read by indexarr[i]list.get(i)
Write by indexarr[i] = xlist.set(i, x)
Lengtharr.length (field)list.size() (method)
Print readablyArrays.toString(arr)list directly

ArrayList's built-in toString (the readable [Edge, Chrome, ...]) is a small thing that adds up — every println(list) is debuggable without a helper.

Iterating

The enhanced for loop works exactly as it did for arrays:

for (String browser : browsers) {
    System.out.println("Testing on: " + browser);
}

When you need the index, the indexed form is available too:

for (int i = 0; i < browsers.size(); i++) {
    System.out.println((i + 1) + ". " + browsers.get(i));
}

Most QA loops use the for-each form. Reach for the indexed version only when you genuinely need i (for numbering, parallel iteration, or modifying by index).

Program to the interface — List<String>

A small but important idiom. Declare the variable as List<String> (the interface) but build it as new ArrayList<>() (the implementation):

List<String> browsers = new ArrayList<>();

This is the polymorphism story from chapter 5.3 applied to collections. Anywhere you might one day swap to LinkedList or some other List implementation, you change one line — the right-hand side of the assignment. Methods you write to take List<String> accept any list type, not just ArrayList. Java's collections framework is designed to be used through the List, Set, Map, Collection interfaces — the concrete classes are implementation details.

A real QA example — collecting and filtering test results

import java.util.ArrayList;
import java.util.List;
 
public class FailureCollector {
 
    static class Result {
        String name;
        boolean passed;
        Result(String name, boolean passed) { this.name = name; this.passed = passed; }
    }
 
    public static void main(String[] args) {
        List<Result> results = new ArrayList<>();
        results.add(new Result("Login",    true));
        results.add(new Result("Search",   true));
        results.add(new Result("Checkout", false));
        results.add(new Result("Logout",   true));
        results.add(new Result("Export",   false));
 
        List<String> failures = new ArrayList<>();
        int passed = 0;
 
        for (Result r : results) {
            if (r.passed) passed++;
            else failures.add(r.name);
        }
 
        System.out.println("Total:    " + results.size());
        System.out.println("Passed:   " + passed);
        System.out.println("Failures: " + failures);
    }
}

Output:

Total:    5
Passed:   3
Failures: [Checkout, Export]

Two ArrayLists in 30 lines: one to hold every result, one to collect failure names as we walk. Both grow dynamically; neither requires you to declare a size up front. This shape — iterate, accumulate into a list — is half of every test reporter ever written.

Array vs ArrayList — when to use each

Array vs ArrayList — pick the right tool

Array — fixed-size, primitives ok

  • Size set at creation; can't grow

  • Holds primitives directly: int[], double[], boolean[]

  • arr[i] for read/write; arr.length is a field

  • Use when: known size, performance-critical primitive math, low-level interop

  • QA example: a fixed parameter table (4 browsers × 3 resolutions)

ArrayList — dynamic, objects only

  • Grows as you add; no preset size

  • Holds objects only: ArrayList<Integer>, ArrayList<TestUser>

  • list.get(i) / list.set(i, v) / list.add / list.remove; size() is a method

  • Use when: size unknown, need add/remove, want rich methods

  • QA example: collect failures as tests run, build a result list, dynamic test queue

For everyday QA code, default to ArrayList. Reach for raw arrays only when you need primitive types directly (int[] for a tight numeric loop), when you have a hard-coded fixture, or when an external library hands you one.

A note on primitives — autoboxing

ArrayList<int> doesn't compile. ArrayList only holds objects. To store integers, use the wrapper class Integer:

List<Integer> codes = new ArrayList<>();
codes.add(200);            // autoboxed: int → Integer behind the scenes
int first = codes.get(0);  // unboxed: Integer → int

Java does the conversion automatically — that's autoboxing (int → Integer) and unboxing (Integer → int). It's mostly invisible. The two cases where it bites: very large numeric arrays where the Integer wrapper objects waste memory, and tight loops where boxing/unboxing every iteration is measurable. For ordinary QA code, it doesn't matter.

⚠️ Common mistakes

  • Mixing arr.length and list.size(). Arrays use a field with no parentheses; lists use a method with parentheses. Remember which container you're holding. The compiler error is concrete (cannot find symbol — method length()) but only useful once you've seen it twice.
  • Modifying a list while iterating with for-each. for (String b : browsers) browsers.remove(b); throws ConcurrentModificationException at runtime. Use an Iterator (lesson 4) or build a new list of survivors and replace the old one.
  • Treating remove(int) and remove(Object) interchangeably for ArrayList<Integer>. list.remove(2) removes index 2; list.remove(Integer.valueOf(2)) removes the value 2. The two overloads are easy to confuse — for Integer lists, prefer the explicit Integer.valueOf(...) form.

🎯 Practice task

Build a real test result aggregator. 25-30 minutes.

  1. Create ResultsList.java and import java.util.ArrayList; plus import java.util.List;.
  2. Inside the class, define a small static nested class Result with two fields, String name and boolean passed, and a constructor.
  3. In main, create List<Result> results = new ArrayList<>(); and add at least six Result entries — mix passes and failures.
  4. Walk the list with for-each and tally int passed = 0, int failed = 0.
  5. While walking, add the name of each failure to a List<String> failureNames. Print the failure list with one println — confirm it formats nicely ([Checkout, Export]).
  6. Use failureNames.size() for the printed summary. Use failureNames.contains("Checkout") to demonstrate fast membership checks.
  7. Add failureNames.remove("Checkout") and reprint. Confirm one entry is gone.
  8. Stretch: convert your variable type from List<Result> to ArrayList<Result> and back. Notice that List doesn't expose ArrayList's capacity-tuning methods like ensureCapacity or trimToSize. That's the trade-off of programming to the interface — you give up a few implementation-specific methods to gain the freedom to swap implementations later.

You can now grow lists dynamically. Lesson 2 introduces HashMap — the key-value structure every config loader, header bag, and lookup table is built on.

// tip to track lessons you complete and pick up where you left off across devices.