AlgoMaster Logo

Sequenced Collections

Low Priority12 min readUpdated September 21, 2026
Listen to this chapter
Unlock Audio

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.

The Problem Before Java 21

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.

The Three New Interfaces

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:

InterfaceMethods 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.

SequencedCollection in Practice

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.

SequencedSet and LinkedHashSet

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.

SequencedMap and LinkedHashMap

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.

The reversed() View

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.

A Top-Sellers Leaderboard with TreeMap

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.

Empty Collections Throw

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.

Immutable Sequenced Views

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.

Migration Patterns

For code that predates Java 21, here's how the older patterns map to the new API:

Before Java 21Java 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-iteratelist.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.

Performance Summary

All retrofitted types get O(1) performance for the new operations.

TypegetFirst / getLastaddFirst / addLastremoveFirst / removeLastreversed()
ArrayListO(1)addLast O(1), addFirst O(n)removeLast O(1), removeFirst O(n)O(1) view
LinkedListO(1)O(1)O(1)O(1) view
ArrayDequeO(1)O(1) amortizedO(1)O(1) view
LinkedHashSetO(1)O(1)O(1)O(1) view
TreeSetO(log n)inserts in sorted order, O(log n)O(log n)O(1) view
LinkedHashMapO(1)O(1)O(1)O(1) view
TreeMapO(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.

Quiz

Sequenced Collections (Java 21) Quiz

10 quizzes