Appearance
Java 21 Sequenced Collections: SequencedCollection, SequencedSet, and SequencedMap
Java 21 introduced three brand new interfaces into the Java Collections Framework: SequencedCollection, SequencedSet, and SequencedMap. These additions solve a very real and longstanding inconsistency problem that developers have lived with for years. To truly appreciate why these interfaces matter, you need to understand the pain they eliminate. So let us start there.
The Problem Before Java 21
Imagine you have a list of items and you want to fetch the first element. You write list.get(0). Simple. Now you want the last element. You write list.get(list.size() - 1). A bit clunky, but fine.
Now you switch to a Deque because your use case demands it. To get the first element you call deque.getFirst(). To get the last element you call deque.getLast(). Completely different methods from what you used with a list.
Now you switch to a LinkedHashSet because you need ordered unique elements. How do you get the first element now? There is no getFirst() method. There is no get(0) method either. You have to write something like this:
java
LinkedHashSet<String> set = new LinkedHashSet<>();
set.add("apple");
set.add("banana");
set.add("cherry");
// Getting the first element - the old painful way
String first = set.iterator().next();
// Getting the last element - even more painful
String last = null;
for (String s : set) {
last = s;
}You have to iterate through the entire collection just to grab the last element. That is deeply unsatisfying and error prone.
What about TreeMap or LinkedHashMap? Getting the first or last entry requires yet another approach. For TreeMap you can use firstKey() or lastKey(). For LinkedHashMap you again resort to iteration.
Let us put this inconsistency in a table so you can see exactly how bad it was:
| Collection Type | Get First | Get Last | Add First | Add Last | Reverse |
|---|---|---|---|---|---|
| List | get(0) | get(size - 1) | add(0, e) | add(e) | Collections.reverse() |
| Deque | getFirst() | getLast() | addFirst(e) | addLast(e) | descendingIterator() |
| LinkedHashSet | iterator().next() | manual loop | not supported simply | add(e) | create a new reversed structure |
| SortedSet | first() | last() | not meaningful | not meaningful | descendingSet() |
| LinkedHashMap | iterate to get entry | iterate to get entry | complex | complex | no built in way |
Every single collection type does things slightly differently. If you want to write a utility method that works on any ordered collection, you cannot. There is no common interface that guarantees you can access the first or last element in a uniform way. This is the gap that Java 21 closes.
What Makes a Collection "Sequenced"?
Before you can understand the new interfaces, you need to understand what qualifies a collection to be called sequenced. There are three key conditions.
Condition 1: Predictable Iteration Order
A sequenced collection must have a predictable, well defined order. There are two kinds of ordering that qualify:
- Insertion order: the collection remembers the order in which you added elements. A
Listand aLinkedHashSetboth maintain insertion order. - Sorted order: the collection keeps elements in a sorted sequence, such as ascending alphabetical or numerical order. A
TreeSetorTreeMapworks this way.
The keyword is predictable. You must be able to say with certainty which element is first and which is last.
Condition 2: First and Last Are Well Defined
Because the order is predictable, you can meaningfully talk about a "first" element and a "last" element. This is required for getFirst(), getLast(), addFirst(), addLast(), removeFirst(), and removeLast() to make sense.
Condition 3: Reversibility
If you know what first and last mean, you can reverse the sequence. A reversed view of the collection should be possible.
Now look at which collections do NOT qualify:
QueueandPriorityQueue: APriorityQueuedetermines ordering based on priority, not insertion order or a simple sorted sequence. You cannot meaningfully define "add at first" because where an element sits depends entirely on its priority weight. SoQueueandPriorityQueueare excluded.HashSet: AHashSetoffers no predictable ordering at all. Elements could be stored anywhere based on their hash code. You cannot define first or last, so reversing is also meaningless.HashSetis excluded.
This is why when you look at the updated Java collections hierarchy, you will notice that SequencedCollection does not sit above Queue or HashSet. It only covers the collections that can guarantee a meaningful sequence.
Where Do the New Interfaces Fit?
Here is the updated hierarchy in simplified form:
Iterable
└── Collection
└── SequencedCollection <-- NEW
├── List
│ ├── ArrayList
│ └── LinkedList
├── Deque
│ ├── ArrayDeque
│ └── LinkedList
└── SequencedSet <-- NEW
├── LinkedHashSet
└── SortedSet
└── TreeSet
Map
└── SequencedMap <-- NEW
├── LinkedHashMap
└── SortedMap
└── TreeMapSequencedCollection sits directly under Collection and above List and Deque. Then SequencedSet extends SequencedCollection and sits above LinkedHashSet and SortedSet. And SequencedMap is a separate interface that sits above LinkedHashMap and SortedMap.
You might wonder why SequencedSet is a separate interface rather than just letting LinkedHashSet extend SequencedCollection directly. The reason is duplicates. A List and a Deque allow duplicate elements. A Set by definition does not. So SequencedSet needs to express this no duplicates constraint separately, inheriting from both SequencedCollection and Set. It is a clean separation of concerns.
The SequencedCollection Interface
The SequencedCollection interface is defined like this:
java
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() {
// ...
}
default E removeFirst() {
// ...
}
default E removeLast() {
// ...
}
}Notice that most methods are default methods, meaning they have a fallback implementation. The only abstract method is reversed(), which every implementing class must provide its own version of.
Let us walk through each method in detail.
getFirst() and getLast()
These give you the first and last elements of the collection without any awkward index arithmetic or iteration.
java
import java.util.ArrayList;
import java.util.List;
public class SequencedDemo {
public static void main(String[] args) {
List<String> fruits = new ArrayList<>();
fruits.add("apple");
fruits.add("banana");
fruits.add("cherry");
// Before Java 21 you had to write:
String oldFirst = fruits.get(0);
String oldLast = fruits.get(fruits.size() - 1);
// After Java 21:
String first = fruits.getFirst(); // "apple"
String last = fruits.getLast(); // "cherry"
System.out.println("First: " + first);
System.out.println("Last: " + last);
}
}The same methods work uniformly on a Deque:
java
import java.util.ArrayDeque;
import java.util.Deque;
Deque<Integer> deque = new ArrayDeque<>();
deque.add(10);
deque.add(20);
deque.add(30);
System.out.println(deque.getFirst()); // 10
System.out.println(deque.getLast()); // 30And on a LinkedHashSet:
java
import java.util.LinkedHashSet;
LinkedHashSet<String> names = new LinkedHashSet<>();
names.add("Zara");
names.add("Mia");
names.add("Leo");
// No more iterator tricks!
System.out.println(names.getFirst()); // "Zara"
System.out.println(names.getLast()); // "Leo"One consistent API across all these different collection types. That is the power of SequencedCollection.
addFirst() and addLast()
These let you insert elements at the beginning or end of the sequence.
java
List<String> cities = new ArrayList<>();
cities.add("London");
cities.add("Tokyo");
cities.addFirst("Paris"); // Insert at the beginning
cities.addLast("Sydney"); // Insert at the end
System.out.println(cities);
// Output: [Paris, London, Tokyo, Sydney]For a List, addFirst("Paris") is equivalent to add(0, "Paris"). For a Deque, it calls push() or addFirst() from the old Deque API. The new interface just standardizes the name.
Note an important nuance for SortedSet: calling addFirst() or addLast() on a TreeSet always throws UnsupportedOperationException, even if the element is the smallest or largest. A sorted set decides the position itself, so "put this first" makes no sense. Just use add(e): the smallest value ends up first and the largest last automatically.
On a LinkedHashSet, addFirst() and addLast() work, and since a set has no duplicates, adding an element that is already present moves it:
java
SequencedSet<String> set = new LinkedHashSet<>(List.of("B", "C", "D"));
set.addFirst("A"); // [A, B, C, D]
set.addLast("Z"); // [A, B, C, D, Z]
set.addFirst("C"); // [C, A, B, D, Z] (C moved to the front)removeFirst() and removeLast()
These remove and return the first or last element.
java
List<String> queue = new ArrayList<>();
queue.add("task1");
queue.add("task2");
queue.add("task3");
String removed = queue.removeFirst(); // "task1"
System.out.println(removed);
System.out.println(queue); // [task2, task3]
String lastRemoved = queue.removeLast(); // "task3"
System.out.println(lastRemoved);
System.out.println(queue); // [task2]This is especially useful in processing pipelines where you consume elements from either end.
reversed()
This is the only abstract method in the interface, meaning every implementing class provides its own implementation. The key insight is that reversed() returns a view, not a copy. This is important for performance.
java
List<Integer> numbers = new ArrayList<>();
numbers.add(1);
numbers.add(2);
numbers.add(3);
numbers.add(4);
numbers.add(5);
// Get a reversed view
List<Integer> reversedView = numbers.reversed();
System.out.println(reversedView);
// Output: [5, 4, 3, 2, 1]
// If you modify the original, the view reflects the change
numbers.add(6);
System.out.println(reversedView);
// Output: [6, 5, 4, 3, 2, 1]Because it is a view backed by the original collection, modifications to either the original or the reversed view are reflected in both. This is efficient because no data is actually copied. Think of it like a mirror: the mirror does not duplicate the room, it just shows it from the other direction.
If you want an independent reversed copy, you need to explicitly create one:
java
List<Integer> reversedCopy = new ArrayList<>(numbers.reversed());The SequencedSet Interface
SequencedSet extends both SequencedCollection and Set. Its definition is concise:
java
public interface SequencedSet<E> extends SequencedCollection<E>, Set<E> {
@Override
SequencedSet<E> reversed();
}The only thing it overrides is the return type of reversed(). When you call reversed() on a SequencedSet, you get back a SequencedSet, not just a SequencedCollection. This is important because you want to preserve the type information.
java
import java.util.LinkedHashSet;
LinkedHashSet<String> colors = new LinkedHashSet<>();
colors.add("red");
colors.add("green");
colors.add("blue");
// getFirst and getLast work perfectly
System.out.println(colors.getFirst()); // "red"
System.out.println(colors.getLast()); // "blue"
// Reverse is a view of the set in reverse insertion order
var reversed = colors.reversed();
System.out.println(reversed);
// Output: [blue, green, red]For TreeSet, which follows sorted order rather than insertion order, the reversed() method gives you a descending view:
java
import java.util.TreeSet;
TreeSet<Integer> numbers = new TreeSet<>();
numbers.add(14);
numbers.add(5);
numbers.add(7);
// TreeSet stores in sorted ascending order: [5, 7, 14]
System.out.println(numbers.getFirst()); // 5
System.out.println(numbers.getLast()); // 14
// Reversed gives descending view
System.out.println(numbers.reversed()); // [14, 7, 5]Remember: addFirst() and addLast() on a TreeSet always throw UnsupportedOperationException. The same goes for putFirst()/putLast() on a TreeMap; on a LinkedHashMap they work.
The SequencedMap Interface
SequencedMap is a separate hierarchy because Map does not extend Collection. Its definition looks like this:
java
public interface SequencedMap<K, V> extends Map<K, V> {
SequencedMap<K, V> reversed();
default Map.Entry<K, V> firstEntry() { ... }
default Map.Entry<K, V> lastEntry() { ... }
default Map.Entry<K, V> pollFirstEntry() { ... }
default Map.Entry<K, V> pollLastEntry() { ... }
default K firstKey() { ... }
default K lastKey() { ... }
default V putFirst(K k, V v) { ... }
default V putLast(K k, V v) { ... }
default SequencedSet<K> sequencedKeySet() { ... }
default SequencedCollection<V> sequencedValues() { ... }
default SequencedSet<Map.Entry<K, V>> sequencedEntrySet() { ... }
}The map equivalent of getFirst is firstEntry() and lastEntry(), which return a Map.Entry giving you both the key and value. pollFirstEntry() and pollLastEntry() remove and return those entries.
The sequencedKeySet(), sequencedValues(), and sequencedEntrySet() methods return views of the map's keys, values, and entries as sequenced collections, so you can use all the SequencedCollection methods on them.
java
import java.util.LinkedHashMap;
LinkedHashMap<String, Integer> scores = new LinkedHashMap<>();
scores.put("Alice", 95);
scores.put("Bob", 87);
scores.put("Carol", 92);
// Get first and last entries
System.out.println(scores.firstEntry()); // Alice=95
System.out.println(scores.lastEntry()); // Carol=92
// Get keys as a sequenced set
scores.sequencedKeySet().forEach(System.out::println);
// Alice, Bob, Carol
// Reversed view
System.out.println(scores.reversed().firstEntry()); // Carol=92For TreeMap, which stores keys in sorted order:
java
import java.util.TreeMap;
TreeMap<String, Integer> sorted = new TreeMap<>();
sorted.put("banana", 2);
sorted.put("apple", 1);
sorted.put("cherry", 3);
// TreeMap sorts alphabetically
System.out.println(sorted.firstEntry()); // apple=1
System.out.println(sorted.lastEntry()); // cherry=3
System.out.println(sorted.firstKey()); // "apple"
System.out.println(sorted.lastKey()); // "cherry"A Complete Before and After Comparison
Here is a side by side comparison showing exactly what changed for common operations:
java
import java.util.*;
public class BeforeAndAfter {
public static void main(String[] args) {
// === LIST ===
List<String> list = new ArrayList<>(List.of("a", "b", "c"));
// Before Java 21
String oldListFirst = list.get(0);
String oldListLast = list.get(list.size() - 1);
// After Java 21
String newListFirst = list.getFirst(); // "a"
String newListLast = list.getLast(); // "c"
// === DEQUE ===
Deque<String> deque = new ArrayDeque<>(List.of("x", "y", "z"));
// Before Java 21 - already had getFirst/getLast but only on Deque
String dequeFirst = deque.getFirst(); // "x"
// After Java 21 - same methods, now unified under SequencedCollection
String dequeFirstNew = deque.getFirst(); // "x" - same
// === LINKED HASH SET ===
LinkedHashSet<String> lhs = new LinkedHashSet<>(List.of("p", "q", "r"));
// Before Java 21 - painful
String oldSetFirst = lhs.iterator().next();
String oldSetLast = null;
for (String s : lhs) oldSetLast = s;
// After Java 21 - clean
String newSetFirst = lhs.getFirst(); // "p"
String newSetLast = lhs.getLast(); // "r"
System.out.println("List first: " + newListFirst);
System.out.println("Set first: " + newSetFirst);
System.out.println("Set last: " + newSetLast);
}
}Check yourself
- Before Java 21, how did you get the last element of a
LinkedHashSet? - What three conditions make a collection "sequenced"?
- Why are
HashSetandPriorityQueuenot sequenced? - Which method of
SequencedCollectionis abstract? Which are default? - Does
list.reversed()return a copy? What happens to it if you add to the original list? - What does
treeSet.addFirst(1)do? - Which map methods correspond to
getFirst,removeFirstandaddFirst?
Answers
- Loop over the whole set and keep the last value (or build a reversed copy).
- A predictable order (insertion or sorted), a well-defined first and last element, and the ability to reverse.
HashSethas no predictable order; aPriorityQueueorders by priority, so "add first" makes no sense.reversed()is abstract;addFirst,addLast,getFirst,getLast,removeFirst,removeLastare default methods.- No, it is a view; the change shows up in the reversed view too.
- Throws
UnsupportedOperationException, always. Useadd. firstEntry(),pollFirstEntry(),putFirst(k, v).