ArrayList
ArrayList is a resizable list backed by an array. It keeps elements in insertion order, allows duplicates, gives fast access by index, and grows automatically as elements are added. It is the most widely used collection in Java, and the default choice whenever a program needs an ordered list whose size is not known in advance.
Why ArrayList Exists
Arrays have a fixed length. To add an element to a full array you must create a bigger one and copy everything, and to remove an element you must shift the others by hand. ArrayList does all of this internally:
String[] arr = new String[2];
arr[0] = "Ada";
arr[1] = "Linus";
// arr[2] = "Grace"; // ArrayIndexOutOfBoundsException: the array is full
List<String> list = new ArrayList<>();
list.add("Ada");
list.add("Linus");
list.add("Grace"); // grows automatically
Creating an ArrayList
import java.util.ArrayList;
import java.util.List;
List<String> names = new ArrayList<>(); // empty
List<Integer> scores = new ArrayList<>(1000); // empty, room for 1000 before growing
List<String> copy = new ArrayList<>(List.of("a", "b")); // copy of another collection
Declare the interface type
The variable is usually declared as List<String>, not ArrayList<String>:
List<String> names = new ArrayList<>();
Code that uses names then depends only on the List interface. Switching to another implementation later changes one line. Method parameters and return types should also use List for the same reason.
Generics and the diamond operator
<String> is a type argument: this list holds only String values, and the compiler enforces it.
names.add("Ada");
names.add(42); // compile error: int cannot be converted to String
String first = names.get(0); // no cast needed
new ArrayList<>() uses the diamond operator: the compiler infers <String> from the variable's declared type.
Lists of primitives
Collections hold objects only. For numbers, use the wrapper classes; Java converts automatically (autoboxing):
List<Integer> ages = new ArrayList<>();
ages.add(30); // int 30 is boxed to Integer
int a = ages.get(0); // unboxed back to int
List<int> does not compile. For very large amounts of numeric data, a primitive array (int[]) uses far less memory than a List<Integer>, since each Integer is a separate object.
Adding Elements
List<String> tasks = new ArrayList<>();
tasks.add("Write tests"); // append to the end
tasks.add("Fix bug");
tasks.add(0, "Review PR"); // insert at index 0; others shift right
tasks.addAll(List.of("Deploy", "Update docs"));
System.out.println(tasks);
// [Review PR, Write tests, Fix bug, Deploy, Update docs]
add(index, element) accepts any index from 0 to size(). An index of size() appends; anything larger throws IndexOutOfBoundsException.
Accessing and Updating
String first = tasks.get(0); // "Review PR"
String last = tasks.get(tasks.size() - 1); // "Update docs"
tasks.set(2, "Fix login bug"); // replace; returns the old element
int count = tasks.size(); // 5
boolean none = tasks.isEmpty(); // false
size() is the number of elements actually in the list (unlike an array's length, which is its capacity). Valid indexes are 0 to size() - 1.
Searching
tasks.contains("Deploy"); // true
tasks.indexOf("Deploy"); // 3, or -1 if absent
tasks.lastIndexOf("Deploy"); // last occurrence
These methods compare with equals, so they work correctly for String, wrapper classes, records, and any class that overrides equals. They scan the list from the start, taking time proportional to its size.
Removing Elements
tasks.remove("Deploy"); // removes the first matching element; returns true/false
tasks.remove(0); // removes by index; returns the removed element
tasks.removeIf(t -> t.startsWith("Update")); // removes every element matching a condition
tasks.clear(); // removes everything
The remove(int) versus remove(Object) trap
For a List<Integer>, remove is overloaded, and an int argument selects the index version:
List<Integer> nums = new ArrayList<>(List.of(10, 20, 30));
nums.remove(1); // removes index 1 → [10, 30]
nums.remove(Integer.valueOf(10)); // removes the value 10 → [30]
Wrap the value in Integer.valueOf(...) when you mean "remove this number."
Iterating
List<String> langs = new ArrayList<>(List.of("Java", "Bash", "C"));
for (String lang : langs) { // for-each
System.out.println(lang);
}
for (int i = 0; i < langs.size(); i++) { // by index
System.out.println(i + ": " + langs.get(i));
}
langs.forEach(System.out::println); // forEach with a method reference
Do not modify the list inside a for-each loop
for (String lang : langs) {
if (lang.equals("Java")) {
langs.remove(lang); // ConcurrentModificationException on the next iteration
}
}
The for-each loop uses an iterator, which detects that the list was changed by other code and, in most cases, fails fast with ConcurrentModificationException. The check is best-effort, not guaranteed: removing the second-to-last element (here "Bash") happens to end the loop silently without an exception, and the last element is simply never visited. Because the behavior depends on position, never rely on it. Use removeIf, or an explicit Iterator and its own remove() method:
langs.removeIf(lang -> lang.equals("Bash"));
Iterator<String> it = langs.iterator();
while (it.hasNext()) {
if (it.next().length() < 2) it.remove();
}
Sorting and Other Bulk Operations
List<String> names = new ArrayList<>(List.of("Linus", "ada", "Grace"));
names.sort(null); // natural order: [Grace, Linus, ada]
names.sort(String.CASE_INSENSITIVE_ORDER); // [ada, Grace, Linus]
names.sort(Comparator.comparing(String::length)); // by length
names.replaceAll(String::toUpperCase); // transform every element in place
Collections.reverse(names);
Collections.shuffle(names);
How ArrayList Works Internally
An ArrayList holds an internal array (its capacity) and a count of elements in use (its size):
size = 3, capacity = 10
[ "A" | "B" | "C" | null | null | null | null | null | null | null ]
addat the end stores into the next free slot. When the array is full,ArrayListallocates a larger one (about 1.5 times the old capacity) and copies the elements over. Because growth is proportional, the average cost of appending stays constant.get(i)andset(i, x)read or write the internal array directly: constant time.add(i, x)andremove(i)in the middle must shift every following element by one position, so they take time proportional to the number of elements afteri.containsandindexOfscan from the start.
| Operation | Time |
|---|---|
get(i), set(i, x) | O(1) |
add(x) at the end | O(1) on average |
add(i, x), remove(i) | O(n) (shifts elements) |
contains, indexOf, remove(Object) | O(n) |
If you know roughly how many elements you will add, passing an initial capacity (new ArrayList<>(10_000)) avoids repeated regrowth. trimToSize() shrinks the internal array to the current size when memory matters.
Mutable and Unmodifiable Lists
List.of(...) and stream.toList() create unmodifiable lists. Calling add, remove, or set on them throws UnsupportedOperationException:
List<String> fixed = List.of("a", "b");
fixed.add("c"); // UnsupportedOperationException
List<String> editable = new ArrayList<>(fixed);
editable.add("c"); // fine
Wrapping in new ArrayList<>(...) gives a modifiable copy. Conversely, return List.copyOf(list) or Collections.unmodifiableList(list) from a method when callers should not be able to change your data.
Arrays.asList(array) is a third kind: a fixed-size list backed by the array. set works (and writes through to the array), but add and remove throw.
Converting Between Lists and Arrays
List<String> list = new ArrayList<>(List.of("x", "y", "z"));
String[] array = list.toArray(new String[0]); // list → array
List<String> back = new ArrayList<>(Arrays.asList(array)); // array → modifiable list
Passing new String[0] tells toArray which array type to create.
ArrayList Versus Other Lists
ArrayList | LinkedList | Array | |
|---|---|---|---|
| Access by index | Fast | Slow (walks the list) | Fast |
| Add/remove at the end | Fast | Fast | Not possible (fixed size) |
| Add/remove at the front or middle | Slow (shifts) | Fast once positioned | Not possible |
| Memory per element | Low | High (node objects) | Lowest |
| Primitives | Boxed | Boxed | Direct |
In practice, ArrayList is the right choice for the large majority of lists. LinkedList is covered in the next lesson; for queue-like use at both ends, ArrayDeque is usually better than either.
Practical Example: Shopping Cart
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
public class Cart {
record Item(String name, double price, int qty) {
double total() { return price * qty; }
}
private final List<Item> items = new ArrayList<>();
void add(String name, double price, int qty) {
for (int i = 0; i < items.size(); i++) {
Item it = items.get(i);
if (it.name().equals(name)) {
items.set(i, new Item(name, price, it.qty() + qty)); // replace with updated quantity
return;
}
}
items.add(new Item(name, price, qty));
}
boolean remove(String name) {
return items.removeIf(it -> it.name().equals(name));
}
double total() {
double sum = 0;
for (Item it : items) sum += it.total();
return sum;
}
List<Item> view() {
return List.copyOf(items);
}
public static void main(String[] args) {
Cart cart = new Cart();
cart.add("Notebook", 80, 3);
cart.add("Pen", 12.5, 10);
cart.add("Backpack", 1499, 1);
cart.add("Pen", 12.5, 5);
cart.remove("Notebook");
List<Item> sorted = new ArrayList<>(cart.view());
sorted.sort(Comparator.comparingDouble(Item::total).reversed());
for (Item it : sorted) {
System.out.printf("%-9s %3d x %8.2f = %9.2f%n", it.name(), it.qty(), it.price(), it.total());
}
System.out.printf("Total: %.2f (%d lines)%n", cart.total(), cart.view().size());
}
}
Output:
Backpack 1 x 1499.00 = 1499.00
Pen 15 x 12.50 = 187.50
Total: 1686.50 (2 lines)
The cart stores Item records in an ArrayList. Adding an existing product finds it by index and uses set to replace it with a new record carrying the combined quantity, because records are immutable. removeIf deletes lines by condition without the risk of ConcurrentModificationException. view() returns an unmodifiable copy, so outside code cannot alter the cart directly; main makes its own modifiable copy to sort for display, leaving the cart's internal order untouched.