The List<E> interface is Java's contract for an ordered, indexed sequence of elements that allows duplicates. It sits one step below Collection in the hierarchy and adds the operations expected from a "sequence": positional access by index, insertion and removal at a specific spot, and the ability to search for where an element lives. This lesson covers what List adds on top of Collection, its four classic implementations, when to use a List versus a Set or a Map, and the three idiomatic ways to walk through a list.
Collection<E> provides bag-style operations: add, remove, contains, size, isEmpty, clear, plus iteration. None of those care about position. A Collection doesn't promise that the first element added comes out first, and it doesn't even guarantee a "what's the third element?" query.
List<E> makes that promise. Every element has a numeric position starting at 0, the order is the insertion order (unless explicitly changed), and duplicates are allowed. Those two guarantees, order plus index, are central to the interface. They unlock a set of methods that wouldn't make sense on a plain Collection.
The index-based view of a list of items in a shopping cart, in the order the customer added them:
Two observations. First, every slot has a fixed numeric address. Second, "Pen" appears twice (at index 1 and index 3), which is fine because List allows duplicates. A Set would have rejected the second "Pen". A Map would have wanted a separate key for each entry. List doesn't care, because each position is its own slot.
The methods List adds for working with positions:
| Method | What it does |
|---|---|
get(int index) | Returns the element at the given index. |
set(int index, E element) | Replaces the element at the index, returns the old one. |
add(int index, E element) | Inserts at the index, shifting later elements right by one. |
remove(int index) | Removes the element at the index, returns it, shifts later elements left. |
indexOf(Object o) | Returns the first index of o, or -1 if not present. |
lastIndexOf(Object o) | Returns the last index of o, or -1 if not present. |
subList(int from, int to) | Returns a view of the slice [from, to). |
listIterator() | Returns an iterator that can walk both directions and modify. |
There are two add and two remove methods. The single-argument add(E) and remove(Object) come from Collection. The index-flavored versions (add(int, E) and remove(int)) are new in List. Java distinguishes them by parameter type, which is why list.remove(0) and list.remove(Integer.valueOf(0)) do very different things on a List<Integer>. That gotcha is covered below.
Time for code. The examples below all use ArrayList as the concrete type, since something has to be instantiated, but every call is a List method. Switching to LinkedList or any other List implementation would not change a single line.
Start with get, set, add(int, E), and remove(int) on a small cart:
Walk through what each call did. get(1) returned "Pen" without changing the cart. set(1, "Highlighter") replaced "Pen" at index 1 and gave back the old value, so the cart stayed the same length. add(1, "Sticky Notes") inserted at index 1 and shifted "Highlighter" and "Eraser" one position to the right. remove(0) deleted "Notebook" and shifted everything else one position to the left.
That shifting is worth pausing on. Inserting or removing at a position that isn't the end forces every later element to move. On an array-backed list like ArrayList, that's a real cost.
add(int, E) and remove(int) are O(n) on ArrayList when the index isn't the end, because elements after the index have to shift. LinkedList makes the shift cheaper but pays a different cost to find the index in the first place.
The two search methods, indexOf and lastIndexOf, are useful when a value can repeat:
indexOf("Pen") walks from index 0 and returns the first match. lastIndexOf("Pen") walks from the end and returns the last match. When the value isn't present, both return -1, which is the universal "not found" marker for index-returning methods in the JDK. Comparison uses equals, so the stored values need a reasonable equals method. For String, that's already taken care of.
subList(int from, int to) is the slicing method. It returns a view of the slice from from (inclusive) to to (exclusive). The word "view" matters: changes to the sub-list flow back into the original.
The slice contained indices 1, 2, and 3 of the cart. Index 4 was excluded because to is exclusive. The call middle.set(0, "Highlighter") set index 0 in the slice, which is index 1 in the original cart, so the cart's "Pen" became "Highlighter". For an independent copy instead of a view, wrap the call: new ArrayList<>(cart.subList(1, 4)).
subList does not copy the elements. It returns a thin view. That's fast, but it also means the original list must not be structurally modified while the sub-list is in use. A cart.add(...) after taking the view causes any method on middle to throw ConcurrentModificationException.
The two remove overloads are easy to mix up on a List<Integer>. The compiler picks an overload by the static type of the argument, not by intent.
stockCounts.remove(1) called remove(int) and deleted the element at index 1, which was 20. Deleting the value 30 required explicit boxing with Integer.valueOf(30) so the compiler would pick remove(Object). Writing stockCounts.remove(30) would have thrown IndexOutOfBoundsException, because the list only has indices 0 to 2. This trap doesn't appear on a List<String> because there's no overload conflict; it only bites on List<Integer>, List<Long>, and similar numeric-wrapper lists.
List is an interface. Using one requires an instance of a concrete class that implements it. Java ships with four standard ones, and each gets its own chapter. Here's the one-line view so the names aren't a mystery when used in examples:
| Implementation | One-line summary |
|---|---|
ArrayList | Backed by a resizable array. Fast indexed access, fast append at the end. The default choice. |
LinkedList | Backed by a doubly-linked list. Fast insert and remove at the ends, slow indexed access. |
Vector | Like ArrayList but every method is synchronized. Legacy from Java 1.0, rarely used today. |
Stack | Extends Vector and adds push/pop/peek. Also legacy; use ArrayDeque instead. |
In practice, ArrayList covers most "I need a list" cases. Use LinkedList for heavy inserting or removing at the front or middle with rare positional indexing. Avoid Vector and Stack in new code; they exist because they shipped with Java 1.0 and removing them would break old code.
The simplified hierarchy:
Iterable is the topmost type. Collection extends it. List extends Collection and adds the index-based methods we covered earlier. ArrayList, LinkedList, and Vector all implement List directly, and Stack is a subclass of Vector so it inherits everything List defines too.
A practical pattern is common in Java code: declare the variable as the interface type, and instantiate a specific class.
The variable cart has the static type List<String>. The runtime object is an ArrayList<String>. Switching later to LinkedList requires changing only the line new ArrayList<>() to new LinkedList<>(). Every method call elsewhere in the program continues to compile, because they all rely on the List contract rather than ArrayList specifics.
Three of the most common collection types are List, Set, and Map. They're easy to mix up because they all hold "a bunch of things". The difference is what each one promises about those things.
| Question | List<E> | Set<E> | Map<K, V> |
|---|---|---|---|
| Does order matter? | Yes, positional. | Usually no. | Keys may or may not be ordered, depending on type. |
| Can it hold duplicates? | Yes. | No, each element is unique. | Keys are unique; values can repeat. |
| How is an element looked up? | By index. | By value (contains check). | By key. |
| Typical use | Sequence of things in some order. | Membership testing or deduplication. | Key-to-value associations. |
Use a List when the order of items matters and duplicates are valid: a shopping cart in the order items were added, a recently-viewed list in browsing order, a sequence of order line items where two units of the same product count separately.
Use a Set for presence checks where duplicates would be a mistake: the unique set of product IDs the customer has wishlisted, the set of countries shipping is supported, the set of tags on a product.
Use a Map for things with a name (key) and a value: product ID to product name, customer email to customer record, coupon code to discount percentage.
The output for recentlyViewed shows insertion order with the duplicate "Notebook" kept. The wishlist line collapsed the duplicate; only one "Notebook" is in the set. The map line looked up the price by product name, which a List would only answer with a linear scan. The exact ordering of a HashSet isn't specified, so either of the wishlist output lines above is a valid run.
For "search this list with indexOf every time someone asks for X" patterns, a Set (for membership) or a Map (for lookup by name) is the better fit. Lists are not a lookup data structure.
There are three idiomatic ways to walk through a list. Each has a job.
The first is the classic index-based for loop. Use it when the index is needed inside the loop body, or to mutate the list at known positions.
This loop has the index i available inside the body, which is useful for "Position 1: Pen" output or for comparing adjacent elements. The cost on ArrayList is negligible because get(i) is O(1). On LinkedList, this same loop is O(n^2) overall because every get(i) walks from the head; for that implementation, use one of the next two patterns instead.
Indexed for loops are fine on ArrayList. On LinkedList, prefer a for-each or an iterator, because get(i) is O(n) and a length-n loop with get(i) becomes O(n^2).
The second is the enhanced for-each loop, which is appropriate when only the elements are needed:
The for-each loop reads more cleanly than the indexed form and works efficiently on every List implementation, because internally it uses the list's iterator. The downside: there's no index variable, and cart.remove(item) from inside the loop triggers a ConcurrentModificationException. For modification during iteration, use a ListIterator.
The third is ListIterator, which is specific to List and is more powerful than the regular Iterator. It can walk forwards or backwards, expose the index of the current element, and modify the list safely during iteration:
The ListIterator exposes nextIndex() for the position of each element on visit, and set(E) replaces the current element without going back to the list itself. cart.set(1, ...) from inside a for-each loop would be unsafe; it.set(...) is the safe equivalent. ListIterator also has hasPrevious() and previous() for walking backwards, and add(E) to insert at the current position. Most code never needs these, but they're available when required.
A small program that uses index-based methods, search, slicing, and iteration on a recently-viewed-products list:
The "most recent" line read the last element using size() - 1 as the index. The indexOf call found the first "Pen" at position 1. The subList view picked the trailing three entries without copying. remove(0) dropped the oldest item, the typical "evict oldest" move for a bounded history. The final indexed for loop printed each remaining entry with its 1-based position.
10 quizzes