List Interface
java.util.List is the interface for ordered collections that allow duplicates and give access by position (index). It defines what every list can do, regardless of how it stores its elements. ArrayList, LinkedList, the immutable lists returned by List.of, and many others all implement it. Writing code against List rather than a specific class is one of the most important habits in Java programming.
Where List Fits in the Collections Framework
Iterable
└── Collection
├── SequencedCollection (Java 21)
│ ├── List ordered, indexed, duplicates allowed
│ │ ├── ArrayList
│ │ ├── LinkedList
│ │ └── CopyOnWriteArrayList, Vector, ...
│ └── Deque
├── Set no duplicates
└── Queue
Map (separate hierarchy: key-value pairs)
A List is a Collection, so it supports everything collections do (add, remove, contains, size, iteration, streams) and adds operations based on position.
What Makes a List a List
| Property | Meaning |
|---|---|
| Ordered | Elements keep a defined sequence, normally the order in which they were added |
| Indexed | Each element has a zero-based position; get(0) is the first |
| Duplicates allowed | The same value may appear many times |
| Positional equality | Two lists are equal if they contain equal elements in the same order |
Programming to the Interface
List<String> names = new ArrayList<>(); // declare as List, choose an implementation
public double average(List<Integer> scores) { ... } // accept any List
public List<String> activeUsers() { ... } // return a List
Declaring variables, parameters, and return types as List means:
- Callers can pass any list implementation (
ArrayList,LinkedList,List.of(...)). - The implementation can be changed in one place without touching other code.
- The code expresses what it needs (an ordered, indexed sequence), not how it is stored.
Use a more specific type only when you need methods that List does not have, such as ArrayList.ensureCapacity or LinkedList's deque methods.
Positional Methods
These are the methods List adds on top of Collection:
| Method | Purpose |
|---|---|
get(int index) | Element at a position |
set(int index, E e) | Replace an element; returns the old one |
add(int index, E e) | Insert, shifting later elements right |
remove(int index) | Remove by position; returns the removed element |
indexOf(Object o) / lastIndexOf(Object o) | First / last position of an element, or -1 |
subList(int from, int to) | A view of a range of the list |
listIterator() | A two-way iterator that can also set and add |
sort(Comparator) | Sort in place (null for natural order) |
replaceAll(UnaryOperator) | Transform every element in place |
Since Java 21, List also inherits from SequencedCollection:
| Method | Purpose |
|---|---|
getFirst(), getLast() | First / last element (throws if empty) |
addFirst(e), addLast(e) | Insert at either end |
removeFirst(), removeLast() | Remove from either end |
reversed() | A reverse-ordered view of the list |
List<String> steps = new ArrayList<>(List.of("plan", "build", "test"));
steps.addFirst("research");
System.out.println(steps.getLast()); // test
System.out.println(steps.reversed()); // [test, build, plan, research]
On older Java versions, use list.get(0) and list.get(list.size() - 1) instead.
Equality and hashCode
The List contract defines equals precisely: two lists are equal if they have the same size and equal elements at every position. The implementation class does not matter:
List<Integer> a = List.of(1, 2, 3);
List<Integer> b = new LinkedList<>(List.of(1, 2, 3));
System.out.println(a.equals(b)); // true
System.out.println(a.equals(List.of(3, 2, 1))); // false: order matters
hashCode is also defined from the elements, so equal lists have equal hash codes and can be used as map keys (as long as they are not modified while in the map).
subList: A View, Not a Copy
subList(from, to) returns a live view of part of the list. The start is inclusive and the end exclusive. Changes through the view affect the original, which makes it a concise way to operate on a range:
List<Integer> nums = new ArrayList<>(List.of(1, 2, 3, 4, 5, 6));
nums.subList(1, 4).clear(); // remove indexes 1, 2, 3
System.out.println(nums); // [1, 5, 6]
Collections.sort(nums.subList(0, 2)); // sort only part of a list
Two rules apply:
- A sublist is valid only while the backing list is not structurally modified (elements added or removed) by other means. Doing so and then using the sublist throws
ConcurrentModificationException. - To keep a slice independently, copy it:
new ArrayList<>(nums.subList(0, 3))orList.copyOf(...).
Mutable, Unmodifiable, and Fixed-Size Lists
Not every List supports every operation. Methods like add and set are optional: an implementation may throw UnsupportedOperationException.
| How created | get | set | add / remove | null elements |
|---|---|---|---|---|
new ArrayList<>() / new LinkedList<>() | ✓ | ✓ | ✓ | ✓ |
List.of(...), List.copyOf(...), stream.toList() | ✓ | ✗ | ✗ | ✗ (toList() allows them) |
Arrays.asList(array) | ✓ | ✓ (writes to the array) | ✗ | ✓ |
Collections.unmodifiableList(list) | ✓ | ✗ | ✗ | As backing list |
List<String> fixed = List.of("a", "b");
fixed.set(0, "z"); // UnsupportedOperationException
The difference between an unmodifiable view and an immutable copy matters:
List<String> source = new ArrayList<>(List.of("a"));
List<String> view = Collections.unmodifiableList(source);
List<String> copy = List.copyOf(source);
source.add("b");
System.out.println(view); // [a, b] the view reflects changes to the source
System.out.println(copy); // [a] the copy does not
Return List.copyOf(...) from a method when callers should receive a snapshot; return an unmodifiable view when they should see live data but not change it.
Choosing a List Implementation
| Implementation | Strengths | Use when |
|---|---|---|
ArrayList | Fast indexed access, compact memory | Default choice for almost all lists |
LinkedList | Fast insert/remove at the ends and at an iterator position | Many insertions while iterating; need List and Deque together |
List.of / List.copyOf | Immutable, compact, thread-safe to share | Constants, return values, data that should not change |
CopyOnWriteArrayList | Thread-safe; iteration never throws | Read-mostly lists shared between threads (such as listener lists) |
Vector | Legacy synchronized list | Avoid in new code |
Common List Operations
List<Integer> scores = new ArrayList<>(List.of(72, 88, 95, 61, 88));
int max = Collections.max(scores); // 95
int freq = Collections.frequency(scores, 88); // 2
scores.sort(Comparator.reverseOrder()); // [95, 88, 88, 72, 61]
List<Integer> top3 = List.copyOf(scores.subList(0, 3)); // [95, 88, 88]
boolean anyFail = scores.stream().anyMatch(s -> s < 40); // false
The Collections utility class, covered later in this section, adds many algorithms that work on any List: sort, reverse, shuffle, swap, binarySearch, frequency, nCopies, and more.
Practical Example: Playlist Tools
import java.util.*;
public class Playlist2 {
static List<String> shuffleKeepingFirst(List<String> songs, long seed) {
List<String> result = new ArrayList<>(songs);
Collections.shuffle(result.subList(1, result.size()), new Random(seed));
return result;
}
static <T> List<List<T>> chunk(List<T> items, int size) {
List<List<T>> pages = new ArrayList<>();
for (int i = 0; i < items.size(); i += size) {
pages.add(List.copyOf(items.subList(i, Math.min(i + size, items.size()))));
}
return pages;
}
public static void main(String[] args) {
List<String> queue = new ArrayList<>(List.of("Intro", "Song A", "Song B", "Song C", "Song D", "Outro"));
queue.add(3, "Ad Break");
queue.removeIf(s -> s.startsWith("Ad"));
queue.replaceAll(String::toUpperCase);
System.out.println("Queue: " + queue);
System.out.println("First/last: " + queue.getFirst() + " / " + queue.getLast());
System.out.println("Reversed: " + queue.reversed());
List<String> middle = queue.subList(1, queue.size() - 1);
System.out.println("Middle: " + middle);
middle.clear();
System.out.println("After clearing middle view: " + queue);
List<String> tracks = List.of("t1", "t2", "t3", "t4", "t5", "t6", "t7");
System.out.println("Pages: " + chunk(tracks, 3));
System.out.println("Shuffled: " + shuffleKeepingFirst(tracks, 42));
}
}
Output (Java 21 or later, since it uses getFirst, getLast, and reversed):
Queue: [INTRO, SONG A, SONG B, SONG C, SONG D, OUTRO]
First/last: INTRO / OUTRO
Reversed: [OUTRO, SONG D, SONG C, SONG B, SONG A, INTRO]
Middle: [SONG A, SONG B, SONG C, SONG D]
After clearing middle view: [INTRO, OUTRO]
Pages: [[t1, t2, t3], [t4, t5, t6], [t7]]
Shuffled: [t1, t3, t2, t6, t7, t5, t4]
Each step uses a List-level operation: positional add, conditional removeIf, in-place replaceAll, and the sequenced methods getFirst, getLast, and reversed. subList returns a view, so clearing middle removes those songs from queue itself. chunk uses subList in the opposite way, copying each slice with List.copyOf so that the pages are independent of the source; because it is written against List<T>, it works for any list of any element type. shuffleKeepingFirst shuffles only the view that excludes the first track, keeping the intro in place. Passing a seeded Random makes the shuffle repeatable, which is useful in tests; the exact shuffled order shown depends on the seed.