Before Java 21, asking "give me the first element" was different for every ordered collection. List used get(0), Deque used peekFirst(), and LinkedHashSet had no way to do it at all without walking the iterator. Sequenced Collections (JEP 431) fix this by adding three new interfaces, SequencedCollection, SequencedSet, and SequencedMap, that give every ordered collection a consistent API for first, last, and reverse access.
Java's collections always had a notion of encounter order. A List is indexed from 0. A LinkedHashSet remembers insertion order. A TreeMap sorts its keys. But the API for working with the "ends" of these ordered structures was inconsistent.
Grabbing the first element from each:
Three different ways to ask the same question. And the last one, the iterator dance, is the only option for LinkedHashSet. There was no getLast for LinkedHashSet either, so finding the most recent entry meant iterating through every element. That's O(n) for a structure that already knows its tail.
Sequenced Collections solve this with one unified vocabulary across all ordered types.
Java 21 adds three interfaces. They sit above existing collection types and define a consistent first/last/reversed API.
The diagram shows how the new interfaces retrofit existing types. SequencedSet extends SequencedCollection and adds the Set contract on top. SequencedMap stands alone because maps aren't collections, but it follows the same first/last pattern.
The method surface for each interface:
| Interface | Methods Added |
|---|---|
SequencedCollection<E> | addFirst(E), addLast(E), getFirst(), getLast(), removeFirst(), removeLast(), reversed() |
SequencedSet<E> | Inherits everything from SequencedCollection. reversed() returns a SequencedSet<E>. |
SequencedMap<K,V> | firstEntry(), lastEntry(), putFirst(K,V), putLast(K,V), pollFirstEntry(), pollLastEntry(), reversed(), sequencedKeySet(), sequencedValues(), sequencedEntrySet() |
That's the whole API. Once you know these seven (or so) methods, you know how to work with the ends of every ordered collection in Java.
A List is the most common SequencedCollection. Consider an e-commerce order history where each new order gets appended to the end.
addFirst on an ArrayList used to require list.add(0, value), which is awkward. The new method reads naturally.
ArrayList.addFirst and removeFirst are O(n) because the array has to shift. If you prepend often, use ArrayDeque or LinkedList, both of which give you O(1) at both ends.
Before Java 21, LinkedHashSet had no API for first/last access. You had to write set.iterator().next() to get the first element, and getLast was effectively impossible without iterating the whole thing.
Consider a "recently viewed products" feature. The data needs unique product IDs in insertion order, with fast access to the oldest and newest entries.
Re-adding PROD-A doesn't change its position because LinkedHashSet only tracks first-insertion order by default. The set still reports PROD-A as both the oldest entry and a member.
LinkedHashSet.getFirst() and getLast() are O(1). They follow the head and tail pointers maintained by the doubly-linked list inside.
TreeSet is also a SequencedSet, ordered by natural ordering or a Comparator:
For a TreeSet, "first" means smallest by the comparator, not first-inserted. Encounter order is whatever the comparator says it is.
Maps get the same treatment with SequencedMap. Both LinkedHashMap and TreeMap implement it.
A common pattern is a "recently viewed" cache where each product ID maps to a timestamp:
putFirst and putLast work as follows. If the key already exists, the entry moves to that end and the value is updated. If it doesn't, it's inserted at that position.
PROD-C was already in the map. putFirst updated its value to 5 and moved it to the head.
pollFirstEntry and pollLastEntry remove and return the entry at that end, or return null if the map is empty. They're handy for queue-style consumption.
reversed() doesn't copy the collection. It returns a view backed by the same data. Mutate either, and the change is visible in the other.
The diagram shows both views pointing at the same underlying data. A write through the reversed view lands in the same place. There's no second copy of the elements.
A concrete example:
addFirst on the reversed view inserts at the front of the reversed order, which is the back of the original. The original list grew at the tail. One physical list, two perspectives.
reversed() is O(1) on retrofitted types. It allocates a thin wrapper, not a new array. The trade-off is that the view stays alive only as long as you don't restructure the backing collection from a different reference in a way that violates its contract.
TreeMap is sorted by key, so firstEntry is the smallest key and lastEntry is the largest. Combined with reversed(), this makes a clean leaderboard.
A leaderboard where keys are unit-sale counts and values are product IDs:
sequencedEntrySet() returns a SequencedSet<Map.Entry<K,V>>, which means you can iterate in encounter order or call getFirst/getLast on it directly. This replaces the older descendingMap().entrySet() pattern.
getFirst() and getLast() on an empty SequencedCollection throw NoSuchElementException. They don't return null.
SequencedMap.firstEntry() and lastEntry() behave differently. They return null for an empty map rather than throwing. That's because they return Map.Entry, which is nullable by convention.
What's wrong with this code?
This throws NoSuchElementException because the set is empty. getFirst doesn't return null for empty collections, so you can't rely on a null check.
Fix:
Always guard with isEmpty() before calling getFirst or getLast. Or catch NoSuchElementException if "empty" is truly exceptional in your code path.
List.of(...), Set.of(...), and Map.of(...) return immutable views that still implement the new interfaces. You can read with the new methods, but mutating calls throw UnsupportedOperationException.
This is consistent with how immutable collections have always behaved. Reads work, writes don't. The benefit is that you can hand an immutable List to a method that expects a SequencedCollection and the reads will work fine.
For code that predates Java 21, here's how the older patterns map to the new API:
| Before Java 21 | Java 21+ |
|---|---|
list.get(0) | list.getFirst() |
list.get(list.size() - 1) | list.getLast() |
list.add(0, x) | list.addFirst(x) |
list.remove(0) | list.removeFirst() |
deque.peekFirst() | deque.getFirst() |
deque.pollLast() | deque.removeLast() (throws if empty) or keep pollLast() (returns null) |
set.iterator().next() | set.getFirst() |
treeMap.firstKey() | treeMap.firstEntry().getKey() |
list.subList(...).clear() to reverse-iterate | list.reversed().forEach(...) |
Collections.reverse(new ArrayList<>(list)) | list.reversed() (view, not a copy) |
treeSet.descendingIterator() | treeSet.reversed().iterator() |
Two things to watch when migrating. First, the new removeFirst and removeLast throw NoSuchElementException on empty collections, while pollFirst/pollLast on Deque return null. Don't swap one for the other without checking the difference. Second, reversed() returns a view, while Collections.reverse mutates in place. The semantics are different even if the resulting iteration looks the same.
The reverse loop reads naturally and doesn't allocate a new list.
All retrofitted types get O(1) performance for the new operations.
| Type | getFirst / getLast | addFirst / addLast | removeFirst / removeLast | reversed() |
|---|---|---|---|---|
ArrayList | O(1) | addLast O(1), addFirst O(n) | removeLast O(1), removeFirst O(n) | O(1) view |
LinkedList | O(1) | O(1) | O(1) | O(1) view |
ArrayDeque | O(1) | O(1) amortized | O(1) | O(1) view |
LinkedHashSet | O(1) | O(1) | O(1) | O(1) view |
TreeSet | O(log n) | inserts in sorted order, O(log n) | O(log n) | O(1) view |
LinkedHashMap | O(1) | O(1) | O(1) | O(1) view |
TreeMap | O(log n) | O(log n) | O(log n) | O(1) view |
TreeSet and TreeMap aren't O(1) because they're trees. But the constant factor on the new methods is the same as on their pre-existing equivalents like first() and last(). There's no extra cost for the unified API.
The one place to be careful is ArrayList.addFirst and removeFirst. Both shift the entire array. For code that prepends often, ArrayDeque is a better fit.
10 quizzes