Sequenced Collections (JEP 431): A Unified API for Ordered Collections
The 30-Year Gap in the Collections API
Java’s Collections Framework has a fundamental inconsistency: there is no uniform way to access the first or last element of an ordered collection. Before Java 21:
// List — index-based
var list = List.of("a", "b", "c");
String first = list.get(0); // first element
String last = list.get(list.size() - 1); // last element — verbose, error-prone
// Deque — special methods
Deque<String> deque = new ArrayDeque<>(List.of("a", "b", "c"));
String first = deque.getFirst(); // works
String last = deque.getLast(); // works, but Deque only
// LinkedHashSet — NO uniform access at all
LinkedHashSet<String> set = new LinkedHashSet<>(List.of("a", "b", "c"));
String first = set.iterator().next(); // iterate, no direct access
// Last element? No way without iterating the whole set
// SortedSet
SortedSet<String> sorted = new TreeSet<>(List.of("a", "b", "c"));
String first = sorted.first(); // different method name than Deque
String last = sorted.last(); // different method name than Deque
Each collection type has different method names, or no method at all. JEP 431 fixes this across the board.
The Three New Interfaces
flowchart TD
SC["SequencedCollection<E>\ngetFirst / getLast\naddFirst / addLast\nremoveFirst / removeLast\nreversed"]
SS["SequencedSet<E>\nextends SequencedCollection + Set\nreversed returns SequencedSet"]
SM["SequencedMap<K,V>\nfirstEntry / lastEntry\nfirstKey / lastKey\npollFirstEntry / pollLastEntry\nreversedMap"]
SC --> SS
SC --> List
SC --> Deque
SS --> LinkedHashSet
SS --> SortedSet --> NavigableSet --> TreeSet
SM --> LinkedHashMap
SM --> SortedMap --> NavigableMap --> TreeMap
SequencedCollection
public interface SequencedCollection<E> extends Collection<E> {
SequencedCollection<E> reversed();
default void addFirst(E e) { throw new UnsupportedOperationException(); }
default void addLast(E e) { throw new UnsupportedOperationException(); }
default E getFirst() {
return this.iterator().next();
}
default E getLast() {
return this.reversed().iterator().next();
}
default E removeFirst() { ... }
default E removeLast() { ... }
}
All methods have sensible defaults using reversed() and iterator(). Implementations override for efficiency.
SequencedSet
public interface SequencedSet<E> extends SequencedCollection<E>, Set<E> {
SequencedSet<E> reversed(); // covariant return — returns SequencedSet
}
SequencedMap
public interface SequencedMap<K,V> extends Map<K,V> {
SequencedMap<K,V> reversed();
Map.Entry<K,V> firstEntry();
Map.Entry<K,V> lastEntry();
K firstKey();
K lastKey();
Map.Entry<K,V> pollFirstEntry();
Map.Entry<K,V> pollLastEntry();
default void putFirst(K k, V v) { ... }
default void putLast(K k, V v) { ... }
}
Using Sequenced Collections
List
List<String> list = new ArrayList<>(List.of("a", "b", "c", "d"));
// New uniform API
String first = list.getFirst(); // "a"
String last = list.getLast(); // "d"
list.addFirst("Z"); // ["Z", "a", "b", "c", "d"]
list.addLast("X"); // ["Z", "a", "b", "c", "d", "X"]
list.removeFirst(); // removes "Z"
list.removeLast(); // removes "X"
// Reversed view — not a copy, live view
for (String s : list.reversed()) {
System.out.print(s + " "); // d c b a
}
// Works with List.of() — unmodifiable
List<String> immutable = List.of("a", "b", "c");
immutable.getFirst(); // "a" — OK
immutable.addFirst("Z"); // UnsupportedOperationException
Deque / ArrayDeque
Deque already had getFirst() / getLast() — now it also has reversed():
Deque<String> deque = new ArrayDeque<>(List.of("a", "b", "c"));
deque.getFirst(); // "a" — existing method, unchanged
deque.getLast(); // "c" — existing method, unchanged
deque.reversed(); // NEW — reversed view
LinkedHashSet
Previously had no clean first/last access. Now:
LinkedHashSet<String> set = new LinkedHashSet<>();
set.add("first");
set.add("second");
set.add("third");
set.getFirst(); // "first" — NEW
set.getLast(); // "third" — NEW
// Reversed iteration
set.reversed().forEach(System.out::println); // third, second, first
LinkedHashMap
LinkedHashMap<String, Integer> map = new LinkedHashMap<>();
map.put("x", 1);
map.put("y", 2);
map.put("z", 3);
map.firstEntry(); // x=1
map.lastEntry(); // z=3
map.firstKey(); // "x"
map.lastKey(); // "z"
map.pollFirstEntry(); // removes and returns x=1
map.pollLastEntry(); // removes and returns z=3
// Reversed view
map.reversed().firstEntry(); // z=3 (was last)
map.putFirst("a", 0); // inserts at beginning
map.putLast("z", 3); // inserts at end
SortedSet / TreeSet
SortedSet already had first() and last() — now it also implements SequencedSet:
TreeSet<Integer> sorted = new TreeSet<>(Set.of(3, 1, 4, 1, 5, 9, 2, 6));
sorted.getFirst(); // 1 — equivalent to sorted.first()
sorted.getLast(); // 9 — equivalent to sorted.last()
// Reversed — ascending becomes descending
sorted.reversed().getFirst(); // 9
The reversed() Method — Important Behaviour
reversed() returns a live view, not a copy:
List<String> original = new ArrayList<>(List.of("a", "b", "c"));
List<String> reversed = original.reversed();
original.add("d");
System.out.println(reversed); // [d, c, b, a] — reflects the addition
flowchart LR
O["Original: [a, b, c, d]"]
R["Reversed view: [d, c, b, a]"]
O <-->|"live bidirectional view"| R
Note["Mutations to original\nreflected in reversed,\nand vice versa"]
Mutations through the reversed view also affect the original:
reversed.removeFirst(); // removes "d" from original
System.out.println(original); // [a, b, c]
Collections Utility Methods Updated
Collections class got new factory methods:
// Unmodifiable wrappers
SequencedCollection<String> unmodifiable =
Collections.unmodifiableSequencedCollection(list);
SequencedSet<String> unmodifiableSet =
Collections.unmodifiableSequencedSet(set);
SequencedMap<String, Integer> unmodifiableMap =
Collections.unmodifiableSequencedMap(map);
Backward Compatibility
JEP 431 was retrofitted into the existing hierarchy using default methods — no existing code breaks. But there is one edge case: if you have a class implementing both List and Deque, the reversed() method now has an ambiguous return type (one returns List, the other returns Deque). This is resolved by the compiler requiring an explicit override.
// If you have:
class MyDequeList<E> implements List<E>, Deque<E> {
// Must override reversed() explicitly to resolve ambiguity
@Override
public MyDequeList<E> reversed() { ... }
}
This scenario is extremely rare — the JDK itself has no classes implementing both.
Before and After: Real-World Comparison
// BEFORE Java 21 — processing a task queue
LinkedHashMap<String, Task> queue = new LinkedHashMap<>();
// ... populate ...
// Get the oldest task (first inserted)
Map.Entry<String, Task> oldest = queue.entrySet().iterator().next();
// Get the newest task (last inserted)
Map.Entry<String, Task> newest = null;
for (Map.Entry<String, Task> e : queue.entrySet()) {
newest = e; // iterate entire map to find last
}
// AFTER Java 21
Map.Entry<String, Task> oldest = queue.firstEntry();
Map.Entry<String, Task> newest = queue.lastEntry();
Key Takeaways
SequencedCollectionaddsgetFirst(),getLast(),addFirst(),addLast(),removeFirst(),removeLast(), andreversed()to all ordered collectionsSequencedMapaddsfirstEntry(),lastEntry(),firstKey(),lastKey(),pollFirstEntry(),pollLastEntry(),putFirst(),putLast(), andreversed()to ordered mapsreversed()returns a live view — mutations are bidirectionalLinkedHashSetandLinkedHashMapfinally have proper first/last access without iterationList,Deque,SortedSet,SortedMap,TreeSet, andTreeMapall implement the new interfaces- All existing code is backward-compatible — the new interfaces use default methods
Next: Virtual Threads (JEP 444) — the biggest concurrency improvement in Java’s history, delivering millions of threads without the overhead of OS threads.