The problem: no uniform first/last API
Before Java 21, every ordered collection type had its own non-uniform way to access the first or last element:
// List:
String first = list.get(0);
String last = list.get(list.size() - 1); // verbose, off-by-one risk
// Deque (ArrayDeque):
String first = deque.peekFirst();
String last = deque.peekLast();
// SortedSet (TreeSet):
String first = sortedSet.first();
String last = sortedSet.last();
// No collection had a unified reverse-iteration API without mutations or ugly iterators
This inconsistency forced every "get the last element" line to be written differently depending on which collection type was in scope — a genuine source of bugs and cognitive overhead.
Sequenced collections (Java 21, JEP 431) fix this with three new interfaces that
provide a uniform getFirst() / getLast() / reversed() API retrofitted onto the
existing hierarchy.
The three new interfaces
SequencedCollection<E>
Extends Collection<E>. Represents any collection with a defined encounter order and
adds:
| Method | Behaviour |
|---|---|
getFirst() | First element; NoSuchElementException if empty |
getLast() | Last element; NoSuchElementException if empty |
addFirst(E) | Insert at beginning (optional — UOE for fixed-size) |
addLast(E) | Insert at end (optional) |
removeFirst() | Remove and return first (optional) |
removeLast() | Remove and return last (optional) |
reversed() | Live reversed-order view |
List<String> list = new ArrayList<>(List.of("a", "b", "c"));
list.getFirst(); // "a"
list.getLast(); // "c"
list.addFirst("z"); // ["z", "a", "b", "c"]
list.addLast("x"); // ["z", "a", "b", "c", "x"]
list.removeFirst(); // "z" — list is now ["a", "b", "c", "x"]
list.removeLast(); // "x" — list is now ["a", "b", "c"]
SequencedSet<E>
Extends both SequencedCollection<E> and Set<E>. No new methods, but guarantees set
semantics (no duplicates) on an ordered collection. Implemented by LinkedHashSet and
TreeSet (via SortedSet).
SequencedSet<String> set = new LinkedHashSet<>(List.of("banana", "apple", "cherry"));
set.getFirst(); // "banana" — insertion order
set.getLast(); // "cherry"
// Reversed view:
set.reversed().forEach(System.out::println); // cherry, apple, banana
SequencedMap<K, V>
Extends Map<K,V> and adds:
| Method | Behaviour |
|---|---|
firstEntry() | First Map.Entry (no removal) |
lastEntry() | Last Map.Entry (no removal) |
pollFirstEntry() | Remove and return first entry |
pollLastEntry() | Remove and return last entry |
putFirst(K, V) | Insert/move to front (optional) |
putLast(K, V) | Insert/move to back (optional) |
reversed() | Live reversed map view |
sequencedKeySet() | SequencedSet<K> |
sequencedValues() | SequencedCollection<V> |
sequencedEntrySet() | SequencedSet<Map.Entry<K,V>> |
SequencedMap<String, Integer> scores = new LinkedHashMap<>();
scores.put("Alice", 90);
scores.put("Bob", 85);
scores.put("Carol", 95);
scores.firstEntry(); // Alice=90
scores.lastEntry(); // Carol=95
scores.putFirst("Zara", 100); // Zara is now the first entry
Which existing collections implement the new interfaces
The new interfaces are retroactively wired into the existing collection hierarchy — you don't need to change your existing code to get the methods:
| Concrete type | Implements |
|---|---|
ArrayList, LinkedList, ArrayDeque | SequencedCollection |
LinkedHashSet | SequencedSet |
TreeSet | SequencedSet (via SortedSet → SequencedSet) |
LinkedHashMap | SequencedMap |
TreeMap | SequencedMap (via SortedMap → SequencedMap) |
Interface hierarchy additions:
ListextendsSequencedCollectionDequeextendsSequencedCollectionSortedSetextendsSequencedSetSortedMapextendsSequencedMap
So List.of(), Collections.unmodifiableList(), and Arrays.asList() all have
getFirst()/getLast() now — they're List instances and List is a
SequencedCollection.
The reversed() method — a live view
reversed() returns a live, write-through view of the collection in reverse order:
List<Integer> list = new ArrayList<>(List.of(1, 2, 3, 4, 5));
List<Integer> rev = list.reversed();
System.out.println(rev); // [5, 4, 3, 2, 1]
list.add(6); // mutate original
System.out.println(rev); // [6, 5, 4, 3, 2, 1] — reflected in view
rev.addFirst(99); // add to reversed view's front (= original's back)
System.out.println(list); // [1, 2, 3, 4, 5, 6, 99]
Because it's a live view, mutations through either handle affect both. If you need an
independent reversed copy, call new ArrayList<>(list.reversed()).
Practical use — iterating in reverse without mutations
Before Java 21, reversing iteration required either mutating the list (Collections.reverse())
or using a backwards ListIterator:
// Old — mutates the list:
Collections.reverse(list);
list.forEach(System.out::println);
Collections.reverse(list); // put it back
// Old — verbose iterator:
var it = list.listIterator(list.size());
while (it.hasPrevious()) System.out.println(it.previous());
// Java 21 — clean:
list.reversed().forEach(System.out::println); // no mutation
// Or with streams:
list.reversed().stream()
.filter(x -> x > 2)
.forEach(System.out::println);
getFirst() vs get(0) — what's the difference?
For List, getFirst() and get(0) return the same value but throw different
exceptions on an empty list:
List<String> empty = new ArrayList<>();
empty.get(0); // IndexOutOfBoundsException: Index: 0, Size: 0
empty.getFirst(); // NoSuchElementException
NoSuchElementException better communicates "there is no first element" versus
IndexOutOfBoundsException which sounds like a bug in index arithmetic. Prefer
getFirst() for clarity, especially in generic code that receives a SequencedCollection
and doesn't know if it's a List.
Immutable and unmodifiable collections
Read-only methods (getFirst, getLast, firstEntry, lastEntry) work on any
SequencedCollection including immutable ones:
List<String> fixed = List.of("x", "y", "z");
fixed.getFirst(); // "x" — fine
fixed.getLast(); // "z" — fine
fixed.addFirst("a"); // UnsupportedOperationException — immutable
Mutation methods (addFirst, removeFirst, putFirst, etc.) respect the collection's
modifiability contract and throw UnsupportedOperationException for unmodifiable/
immutable collections, exactly as add() and remove() already do.
SequencedCollection in method signatures
Use SequencedCollection<E> in a method signature when your code specifically needs
first/last access but doesn't need random index access (get(i)):
// Good — accepts ArrayList, LinkedList, ArrayDeque, or any ordered collection:
static <T> T penultimate(SequencedCollection<T> col) {
if (col.size() < 2) throw new NoSuchElementException();
return col.reversed().stream().skip(1).findFirst().orElseThrow();
}
// Too narrow — only accepts List:
static <T> T penultimate(List<T> list) { ... }
// Too broad — Collection doesn't guarantee order:
static <T> T penultimate(Collection<T> col) { ... } // getFirst() unavailable
SequencedCollection is the right abstraction when you care about order and
first/last but not about array-like index access.
Replacing Deque methods
Deque already had peekFirst()/peekLast() and addFirst()/addLast(). Deque now
extends SequencedCollection, so ArrayDeque has all the new methods too. The SequencedCollection
equivalents are cleaner for non-queue uses:
| Old Deque API | New SequencedCollection API |
|---|---|
deque.peekFirst() | deque.getFirst() |
deque.peekLast() | deque.getLast() |
deque.pollFirst() | deque.removeFirst() |
deque.pollLast() | deque.removeLast() |
Note: peekFirst() returns null for an empty deque; getFirst() throws
NoSuchElementException. Choose based on whether null vs exception is more appropriate.
Before and after — the complete comparison
// BEFORE Java 21:
List<String> list = new ArrayList<>(List.of("a", "b", "c"));
Deque<String> deque = new ArrayDeque<>(list);
TreeSet<String> sorted = new TreeSet<>(list);
String fl = list.get(0); // first from List
String ll = list.get(list.size()-1); // last from List
String fd = deque.peekFirst(); // first from Deque
String ld = deque.peekLast(); // last from Deque
String fs = sorted.first(); // first from SortedSet
String ls = sorted.last(); // last from SortedSet
// AFTER Java 21 — uniform API:
list.getFirst(); // "a"
list.getLast(); // "c"
deque.getFirst(); // "a"
deque.getLast(); // "c"
sorted.getFirst(); // "a"
sorted.getLast(); // "c"
// Reverse without mutation:
list.reversed().forEach(System.out::println); // c, b, a
deque.reversed().forEach(System.out::println); // c, b, a
sorted.reversed().forEach(System.out::println); // c, b, a
Recap
Sequenced collections (Java 21) add three interfaces — SequencedCollection,
SequencedSet, and SequencedMap — that provide a uniform API for first/last
access and reverse iteration across all ordered collections. getFirst()/getLast()
replace get(0) / get(size-1) with clearer semantics and a better exception type.
addFirst()/addLast()/removeFirst()/removeLast() mirror the Deque API on all
List implementations. reversed() returns a live write-through view — not a copy —
that reflects mutations in both directions. The interfaces are retrofitted onto
ArrayList, LinkedHashSet, TreeSet, LinkedHashMap, TreeMap, and their parent
interfaces, so you get the new methods without any code changes.