Java 21 added three interfaces — SequencedCollection, SequencedSet and
SequencedMap — giving every collection with a defined order the same way to ask for its
first and last element, and to iterate backwards. It is a small addition that fixes a
twenty-five-year-old inconsistency.
The inconsistency
Before Java 21, getting the first element depended entirely on which collection you had:
class Demo {
void run() {
List<String> list = new ArrayList<>(List.of("a", "b", "c"));
Deque<String> deque = new ArrayDeque<>(List.of("a", "b", "c"));
SortedSet<String> sorted = new TreeSet<>(List.of("a", "b", "c"));
LinkedHashSet<String> linked = new LinkedHashSet<>(List.of("a", "b", "c"));
System.out.println(list.get(0)); // an index
System.out.println(deque.getFirst()); // a method
System.out.println(sorted.first()); // a differently named method
System.out.println(linked.iterator().next()); // no method at all
}
}
Four ordered collections, four different answers — and the last one is the telling case. A
LinkedHashSet has a perfectly well-defined first element and no way to ask for it. The
last element was worse: iterating the entire set to find it.
The new interfaces
class Demo {
void run() {
List<String> list = new ArrayList<>(List.of("a", "b", "c"));
System.out.println(list.getFirst()); // a
System.out.println(list.getLast()); // c
System.out.println(list.reversed()); // [c, b, a]
list.addFirst("start");
list.addLast("end");
System.out.println(list); // [start, a, b, c, end]
System.out.println(list.removeFirst()); // start
System.out.println(list.removeLast()); // end
}
}
Six methods, on everything ordered. List, Deque,
LinkedHashSet, SortedSet and LinkedHashMap all gained them
without any change to your code — they are default
methods, which is precisely the mechanism that makes adding to an interface possible.
The hierarchy
| Interface | Extends | Implemented by |
|---|---|---|
SequencedCollection<E> | Collection | List,
Deque |
SequencedSet<E> | SequencedCollection,
Set | LinkedHashSet, SortedSet |
SequencedMap<K,V> | Map | LinkedHashMap,
SortedMap |
The useful consequence is that you can now write a method taking any ordered collection:
class Demo {
<E> E newest(SequencedCollection<E> items) {
return items.getLast(); // works for List, Deque, LinkedHashSet, TreeSet
}
void run() {
System.out.println(newest(List.of("a", "b"))); // b
System.out.println(newest(new LinkedHashSet<>(List.of("a", "b")))); // b
}
}
reversed() is a view, not a copy
The detail worth knowing. reversed() returns a view backed by the original,
so it costs nothing to create and it sees later changes:
class Demo {
void run() {
List<String> list = new ArrayList<>(List.of("a", "b"));
List<String> view = list.reversed();
list.add("c");
System.out.println(view); // [c, b, a] — the view saw the addition
view.set(0, "z"); // writing through the view
System.out.println(list); // [a, b, z]
// For an independent copy, materialise it
List<String> copy = new ArrayList<>(list.reversed());
list.add("d");
System.out.println(copy.size()); // 3 — unaffected
}
}
Compare with Collections.reverse(list), which reverses in place and returns nothing.
The view is usually what you want for iterating backwards, and it does not disturb the original.
Maps
class Demo {
void run() {
LinkedHashMap<String, Integer> scores = new LinkedHashMap<>();
scores.put("Ana", 30);
scores.put("Bo", 25);
scores.put("Cy", 35);
System.out.println(scores.firstEntry()); // Ana=30
System.out.println(scores.lastEntry()); // Cy=35
System.out.println(scores.reversed()); // {Cy=35, Bo=25, Ana=30}
scores.putFirst("Zed", 10); // insert at the front
System.out.println(scores.sequencedKeySet().getFirst()); // Zed
}
}
sequencedKeySet, sequencedValues and sequencedEntrySet return
ordered views, which is what lets you get the first key without iterating.
What "sequenced" means, and what it does not
The word is doing precise work here, and it is worth separating three ideas that get conflated.
A collection is sequenced if it has a well-defined encounter order — there is a first element, a last element, and iterating twice gives the same sequence. That is a weaker property than being sorted, and a stronger one than being merely iterable.
class Demo {
void run() {
// Sequenced by insertion — order is what you put in
SequencedSet<String> insertion = new LinkedHashSet<>(List.of("c", "a", "b"));
System.out.println(insertion.getFirst()); // c
// Sequenced by sorting — order is the comparator's
SequencedSet<String> sorted = new TreeSet<>(List.of("c", "a", "b"));
System.out.println(sorted.getFirst()); // a
// NOT sequenced — HashSet has no defined order, so it gains nothing
Set<String> unordered = new HashSet<>(List.of("c", "a", "b"));
// unordered.getFirst(); // does not compile
}
}
That last case is the point of the design. HashSet deliberately does not implement
SequencedSet, because its iteration order is an implementation detail that can change
between runs. Asking a HashSet for its "first" element is a meaningless question, and
the type system now says so rather than letting you write
hashSet.iterator().next() and believe the answer.
The same reasoning applies to HashMap: it is a Map, not a
SequencedMap. If you need order, that is what LinkedHashMap and
TreeMap are for — the point the Collections post
makes about choosing an implementation deliberately.
One thing that can break
These are new methods on very old interfaces, so a class of yours that implements
List and already had its own getFirst() with different semantics now
conflicts with the interface's. It is rare, and the compiler tells you, but it is the one upgrade
hazard here.
There is also a semantic clash worth noting: Deque already had
addFirst and addLast, and its reversed() had to be defined
consistently with descendingIterator(). If you use Deque heavily, check
that the two agree in your code rather than assuming.
Why it matters
None of this was impossible before — list.get(list.size() - 1) works, everyone wrote
it, and everyone got the off-by-one wrong at least once. The value is in removing four different spellings of one idea, and in making
LinkedHashSet and LinkedHashMap usable for the thing people actually use
them for: keeping insertion order and then looking at the ends of it.
Next
Unnamed variables and patterns is next — a use for the underscore.