Part 5 of 15

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

  • SequencedCollection adds getFirst(), getLast(), addFirst(), addLast(), removeFirst(), removeLast(), and reversed() to all ordered collections
  • SequencedMap adds firstEntry(), lastEntry(), firstKey(), lastKey(), pollFirstEntry(), pollLastEntry(), putFirst(), putLast(), and reversed() to ordered maps
  • reversed() returns a live view — mutations are bidirectional
  • LinkedHashSet and LinkedHashMap finally have proper first/last access without iteration
  • List, Deque, SortedSet, SortedMap, TreeSet, and TreeMap all 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.