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:
| Operation | Array | ArrayList |
|---|---|---|
| Add at end | n/a (fixed size) | list.add(x) |
| Insert in middle | n/a | list.add(i, x) |
| Remove | n/a | list.remove(i) or list.remove(value) |
| Read by index | arr[i] | list.get(i) |
| Write by index | arr[i] = x | list.set(i, x) |
| Length | arr.length (field) | list.size() (method) |
| Print readably | Arrays.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 → intJava 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.lengthandlist.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);throwsConcurrentModificationExceptionat runtime. Use anIterator(lesson 4) or build a new list of survivors and replace the old one. - Treating
remove(int)andremove(Object)interchangeably forArrayList<Integer>.list.remove(2)removes index 2;list.remove(Integer.valueOf(2))removes the value 2. The two overloads are easy to confuse — forIntegerlists, prefer the explicitInteger.valueOf(...)form.
🎯 Practice task
Build a real test result aggregator. 25-30 minutes.
- Create
ResultsList.javaandimport java.util.ArrayList;plusimport java.util.List;. - Inside the class, define a small static nested class
Resultwith two fields,String nameandboolean passed, and a constructor. - In
main, createList<Result> results = new ArrayList<>();and add at least sixResultentries — mix passes and failures. - Walk the list with for-each and tally
int passed = 0,int failed = 0. - While walking, add the name of each failure to a
List<String> failureNames. Print the failure list with oneprintln— confirm it formats nicely ([Checkout, Export]). - Use
failureNames.size()for the printed summary. UsefailureNames.contains("Checkout")to demonstrate fast membership checks. - Add
failureNames.remove("Checkout")and reprint. Confirm one entry is gone. - Stretch: convert your variable type from
List<Result>toArrayList<Result>and back. Notice thatListdoesn't exposeArrayList's capacity-tuning methods likeensureCapacityortrimToSize. 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.