Java 21's SequencedCollection, SequencedSet, and SequencedMap — getFirst, getLast, reversed, and the modern collection utilities that tidy up everyday Spring Boot application code.
Java’s collection API has accumulated decades of inconsistency. Getting the last element of a List meant computing list.get(list.size() - 1). Getting the first element of a LinkedHashSet meant set.iterator().next(). Getting the last entry of a LinkedHashMap required iterating the entire entry set to reach the tail. These were minor irritants that appeared constantly in real application code — the kind of thing you write without thinking about and then wince at during a code review.
Java 21 fixed this. The SequencedCollection, SequencedSet, and SequencedMap interfaces introduced in JEP 431 give all ordered collections a consistent API for accessing and manipulating first and last elements. It’s one of those changes where the before-and-after comparison makes you wonder why it took so long.
Three interfaces were added to the collections hierarchy:
SequencedCollection<E> — extends Collection. Provides getFirst(), getLast(), addFirst(E), addLast(E), removeFirst(), removeLast(), and reversed().
SequencedSet<E> — extends SequencedCollection and Set. Same API; reversed() returns a SequencedSet.
SequencedMap<K, V> — extends Map. Provides firstEntry(), lastEntry(), pollFirstEntry(), pollLastEntry(), putFirst(K, V), putLast(K, V), and reversed().
The concrete classes that now implement these interfaces: List (via ArrayList, LinkedList), Deque (via ArrayDeque, LinkedList), LinkedHashSet, and LinkedHashMap. SortedSet and SortedMap (including TreeSet and TreeMap) also implement the sequenced interfaces, since sorted collections have a natural first and last element.
The improvement is most visible in everyday patterns. Here are the patterns I see most often in Spring Boot application code:
Getting the first and last element of a list:
// Before Java 21
String first = items.get(0);
String last = items.get(items.size() - 1);
// Java 21
String first = items.getFirst();
String last = items.getLast();
getFirst() and getLast() throw NoSuchElementException on an empty collection — the same contract as Deque. No more IndexOutOfBoundsException on a zero-size list with a confusing message.
Getting the most recent entry from a LinkedHashMap:
// Before — iterate everything to reach the tail
Map.Entry<String, Order> latest = null;
for (Map.Entry<String, Order> entry : orderHistory.entrySet()) {
latest = entry;
}
// Java 21
Map.Entry<String, Order> latest = orderHistory.lastEntry();
In the Betfair trading framework I run, each market maintains a LinkedHashMap<Long, PricePoint> keyed by timestamp. Getting the most recent price point was always that loop. It’s now priceHistory.lastEntry().getValue().
Processing events in reverse order:
// Before — copy and reverse, or use a ListIterator
List<AuditEvent> reversed = new ArrayList<>(auditTrail);
Collections.reverse(reversed);
reversed.forEach(this::process);
// Java 21 — reversed() returns a view, no copy
auditTrail.reversed().forEach(this::process);
reversed() returns a view of the original collection in reverse order. It is not a copy. Mutations to the view are reflected in the original. This is the right behaviour for most use cases and matches the design of Collections.unmodifiableList and other view-returning methods.
First and last in a LinkedHashSet:
// Before — awkward iterator dance
String firstVisited = visitedPages.iterator().next();
// Getting the last required streaming or iterating
// Java 21
String firstVisited = visitedPages.getFirst();
String lastVisited = visitedPages.getLast();
In a recent Spring Boot service handling ordered DWP events, a common pattern was pulling the latest event from a bounded history buffer:
@Service
public class ClaimEventProcessor {
// LinkedHashMap maintains insertion order — capacity-bounded via removeEldestEntry
private final LinkedHashMap<String, ClaimEvent> recentEvents =
new LinkedHashMap<>(100, 0.75f, false) {
@Override
protected boolean removeEldestEntry(Map.Entry<String, ClaimEvent> eldest) {
return size() > 100;
}
};
public Optional<ClaimEvent> latestEvent(String claimId) {
// Java 21: no iteration needed
Map.Entry<String, ClaimEvent> last = recentEvents.lastEntry();
return last != null && last.getKey().startsWith(claimId)
? Optional.of(last.getValue())
: Optional.empty();
}
public List<ClaimEvent> eventsInReverseChronologicalOrder() {
// reversed() view — no copying
return new ArrayList<>(recentEvents.sequencedValues().reversed());
}
}
sequencedValues() — another Java 21 addition — returns a SequencedCollection view of the map’s values, which itself supports reversed(). Similarly, sequencedKeySet() and sequencedEntrySet() exist for the same reason.
Java 21’s sequenced collections are the most visible change, but several earlier additions are still underused in production codebases:
List.copyOf(collection) (Java 10) — creates an unmodifiable list copy. Cleaner than Collections.unmodifiableList(new ArrayList<>(source)):
// Before
List<String> safe = Collections.unmodifiableList(new ArrayList<>(mutableList));
// Java 10+
List<String> safe = List.copyOf(mutableList);
Map.entry(key, value) (Java 9) — creates an immutable Map.Entry without creating a full map. Useful when you need to return or pass a key-value pair:
// Before
AbstractMap.SimpleEntry<String, Integer> entry =
new AbstractMap.SimpleEntry<>("key", 42);
// Java 9+
Map.Entry<String, Integer> entry = Map.entry("key", 42);
Stream.toList() (Java 16) — replaces collect(Collectors.toList()). The returned list is unmodifiable, which is usually what you want for the result of a stream operation:
// Before
List<String> names = users.stream()
.map(User::name)
.collect(Collectors.toList());
// Java 16+
List<String> names = users.stream()
.map(User::name)
.toList();
Note the semantic difference: Collectors.toList() returns a mutable ArrayList; Stream.toList() returns an unmodifiable list. If the calling code adds to the result, switching to toList() will throw UnsupportedOperationException at runtime — check before replacing.
Map.of(...) and Map.copyOf(...) (Java 9 / Java 10) — for small immutable maps, these are far cleaner than new HashMap<>() with repeated put calls. Maximum 10 entries for Map.of; no limit for Map.copyOf:
Map<String, Integer> statusCodes = Map.of(
"PENDING", 0,
"ACTIVE", 1,
"SUSPENDED", 2,
"CLOSED", 3
);
If you’re on Java 21, the sequenced collection methods are worth adopting immediately wherever you access first or last elements of ordered collections. They’re backward-compatible — no behaviour change, just a cleaner call site. Static analysis tools like IntelliJ IDEA flag the list.get(list.size() - 1) pattern and suggest getLast() automatically.
If you’re still on Java 17 or earlier, the Stream.toList() and List.copyOf improvements are available and worth using now. The sequenced collection interfaces are the compelling reason to move to 21 for greenfield services.
The pattern across all of these improvements is consistent: Java is removing the sharp edges around common collection operations, one release at a time. The code that results is not just shorter — it’s clearer about intent, less likely to produce an off-by-one error, and easier to review.
If you’re modernising a Java codebase to Java 21 and want an experienced engineer who’s done it across multiple enterprise projects, get in touch.