1. Introduction
Java 21 introduces one of the most overdue additions to the Collections Framework: Sequenced Collections. This feature addresses a problem that has annoyed Java developers for over two decades — there was no uniform way to access the first and last elements of an ordered collection.
Think about it. You have a List, a Deque, a SortedSet, and a LinkedHashSet. All four maintain a defined encounter order. All four have a concept of “first element” and “last element.” Yet the method you call to get that first or last element is completely different for each type. It is as if every car manufacturer put the steering wheel in a different location. The car works, but every time you switch cars, you have to relearn how to drive.
Java 21 fixes this with three new interfaces: SequencedCollection, SequencedSet, and SequencedMap. These interfaces provide a unified API for accessing, adding, and removing elements at both ends of any ordered collection, plus a reversed() method that gives you a reversed view without copying.
This is not a minor convenience. It is a fundamental improvement to the Collections Framework that changes how you write collection-handling code. Let us walk through the problem, the solution, and practical applications.
2. The Problem Before Java 21
Before Java 21, getting the first and last element of different collection types required completely different code. There was no common interface, no polymorphism, no way to write a generic method that says “give me the first element of this ordered collection.” Let us look at how bad this was.
2.1 Getting the First Element
Here is how you get the first element from four different collection types — all of which maintain order:
// Getting the first element -- 4 different APIs for the same concept
// List -- use index
List<String> list = List.of("alpha", "beta", "gamma");
String first = list.get(0);
// Deque -- dedicated method
Deque<String> deque = new ArrayDeque<>(List.of("alpha", "beta", "gamma"));
String first = deque.getFirst();
// SortedSet -- yet another method
SortedSet<String> sortedSet = new TreeSet<>(List.of("alpha", "beta", "gamma"));
String first = sortedSet.first();
// LinkedHashSet -- no direct method at all!
LinkedHashSet<String> linkedHashSet = new LinkedHashSet<>(List.of("alpha", "beta", "gamma"));
String first = linkedHashSet.iterator().next(); // ugly2.2 Getting the Last Element
Getting the last element was even worse:
// Getting the last element -- even more inconsistent
// List -- calculate size minus one
List<String> list = List.of("alpha", "beta", "gamma");
String last = list.get(list.size() - 1);
// Deque -- dedicated method
Deque<String> deque = new ArrayDeque<>(List.of("alpha", "beta", "gamma"));
String last = deque.getLast();
// SortedSet -- different name than Deque
SortedSet<String> sortedSet = new TreeSet<>(List.of("alpha", "beta", "gamma"));
String last = sortedSet.last();
// LinkedHashSet -- absolutely terrible
LinkedHashSet<String> linkedHashSet = new LinkedHashSet<>(List.of("alpha", "beta", "gamma"));
String last = null;
for (String s : linkedHashSet) {
last = s; // iterate through EVERYTHING to get the last one
}2.3 Reversing a Collection
Reversing the iteration order was equally inconsistent:
// Reversing -- no common approach
// List -- create a new reversed copy
List<String> list = new ArrayList<>(List.of("alpha", "beta", "gamma"));
Collections.reverse(list); // mutates the original!
// or use ListIterator to go backwards (verbose)
ListIterator<String> it = list.listIterator(list.size());
while (it.hasPrevious()) {
System.out.println(it.previous());
}
// Deque -- use descendingIterator
Deque<String> deque = new ArrayDeque<>(List.of("alpha", "beta", "gamma"));
Iterator<String> descIt = deque.descendingIterator();
// NavigableSet -- descendingSet returns a view
NavigableSet<String> navSet = new TreeSet<>(List.of("alpha", "beta", "gamma"));
NavigableSet<String> reversed = navSet.descendingSet();
// LinkedHashSet -- no built-in way to reverse at all
// You have to copy to a List, reverse it, and create a new LinkedHashSetThis inconsistency made it impossible to write generic utility methods. If you wanted a method getFirst(Collection c) that works with any ordered collection, you could not do it cleanly. You needed instanceof checks everywhere. The lack of a common interface for ordered collections was a fundamental gap in Java’s type system.
3. SequencedCollection Interface
The SequencedCollection interface is the core of this feature. It represents a collection with a defined encounter order — meaning there is a well-defined first element, second element, and so on, through the last element. It extends Collection and adds the following methods:
public interface SequencedCollection<E> extends Collection<E> {
// Returns a reversed-order view of this collection
SequencedCollection<E> reversed();
// First element operations
void addFirst(E e);
void addLast(E e);
E getFirst();
E getLast();
E removeFirst();
E removeLast();
}3.1 Using SequencedCollection Methods
Now every ordered collection speaks the same language:
import java.util.*;
public class SequencedCollectionDemo {
public static void main(String[] args) {
// ArrayList implements SequencedCollection
List<String> list = new ArrayList<>(List.of("alpha", "beta", "gamma"));
System.out.println(list.getFirst()); // alpha
System.out.println(list.getLast()); // gamma
// ArrayDeque implements SequencedCollection
Deque<String> deque = new ArrayDeque<>(List.of("alpha", "beta", "gamma"));
System.out.println(deque.getFirst()); // alpha
System.out.println(deque.getLast()); // gamma
// LinkedHashSet implements SequencedCollection (via SequencedSet)
LinkedHashSet<String> linkedSet = new LinkedHashSet<>(List.of("alpha", "beta", "gamma"));
System.out.println(linkedSet.getFirst()); // alpha
System.out.println(linkedSet.getLast()); // gamma
// TreeSet implements SequencedCollection (via SequencedSet)
TreeSet<String> treeSet = new TreeSet<>(List.of("alpha", "beta", "gamma"));
System.out.println(treeSet.getFirst()); // alpha
System.out.println(treeSet.getLast()); // gamma
}
}Same method, same name, same behavior — regardless of the concrete collection type. This is the polymorphism that was missing for 25 years.
3.2 addFirst() and addLast()
These methods add elements at the beginning or end of the collection:
List<String> languages = new ArrayList<>(List.of("Java", "Python", "Go"));
languages.addFirst("Rust"); // [Rust, Java, Python, Go]
languages.addLast("Kotlin"); // [Rust, Java, Python, Go, Kotlin]
System.out.println(languages);
// Output: [Rust, Java, Python, Go, Kotlin]
// Works with Deque too
Deque<String> stack = new ArrayDeque<>();
stack.addFirst("bottom");
stack.addFirst("middle");
stack.addFirst("top");
System.out.println(stack); // [top, middle, bottom]3.3 removeFirst() and removeLast()
These methods remove and return elements from the ends:
List<String> tasks = new ArrayList<>(List.of("email", "code review", "standup", "deploy"));
String firstTask = tasks.removeFirst(); // "email"
String lastTask = tasks.removeLast(); // "deploy"
System.out.println(firstTask); // email
System.out.println(lastTask); // deploy
System.out.println(tasks); // [code review, standup]
// Throws NoSuchElementException on empty collections
List<String> empty = new ArrayList<>();
try {
empty.getFirst(); // NoSuchElementException
} catch (NoSuchElementException e) {
System.out.println("Collection is empty: " + e.getMessage());
}3.4 Writing Generic Code
The biggest win is that you can now write methods that work with any sequenced collection:
// Generic method that works with ANY sequenced collection
public static <E> void printEndpoints(SequencedCollection<E> collection) {
if (collection.isEmpty()) {
System.out.println("Collection is empty");
return;
}
System.out.println("First: " + collection.getFirst());
System.out.println("Last: " + collection.getLast());
System.out.println("Size: " + collection.size());
}
// Works with all ordered collection types
printEndpoints(new ArrayList<>(List.of(1, 2, 3)));
printEndpoints(new ArrayDeque<>(List.of(1, 2, 3)));
printEndpoints(new LinkedHashSet<>(List.of(1, 2, 3)));
printEndpoints(new TreeSet<>(List.of(1, 2, 3)));Before Java 21, this method would have required either method overloading for each collection type or ugly instanceof checks. Now you just declare the parameter as SequencedCollection and it works everywhere.
4. SequencedSet Interface
SequencedSet extends SequencedCollection and adds set semantics — no duplicate elements are allowed. It also refines the return type of reversed() to return a SequencedSet:
public interface SequencedSet<E> extends Set<E>, SequencedCollection<E> {
@Override
SequencedSet<E> reversed();
}The classes that implement SequencedSet include LinkedHashSet, TreeSet, and ConcurrentSkipListSet. The set-specific behavior affects addFirst() and addLast(): if the element already exists, it is repositioned to the requested end rather than creating a duplicate.
// SequencedSet repositions existing elements
LinkedHashSet<String> colors = new LinkedHashSet<>();
colors.add("red");
colors.add("green");
colors.add("blue");
System.out.println(colors); // [red, green, blue]
// addFirst moves "blue" to the front (no duplicate)
colors.addFirst("blue");
System.out.println(colors); // [blue, red, green]
// addLast moves "blue" to the end
colors.addLast("blue");
System.out.println(colors); // [red, green, blue]
// Adding a new element works as expected
colors.addFirst("yellow");
System.out.println(colors); // [yellow, red, green, blue]4.1 SequencedSet with TreeSet
For sorted sets like TreeSet, addFirst() and addLast() throw UnsupportedOperationException because the position of elements is determined by the sort order, not by insertion order. However, getFirst(), getLast(), removeFirst(), removeLast(), and reversed() all work perfectly:
TreeSet<Integer> scores = new TreeSet<>(List.of(85, 92, 78, 95, 88));
System.out.println(scores); // [78, 85, 88, 92, 95]
System.out.println(scores.getFirst()); // 78 (lowest)
System.out.println(scores.getLast()); // 95 (highest)
// Remove the lowest and highest
int lowest = scores.removeFirst(); // 78
int highest = scores.removeLast(); // 95
System.out.println(scores); // [85, 88, 92]
// addFirst/addLast throw UnsupportedOperationException on TreeSet
try {
scores.addFirst(100);
} catch (UnsupportedOperationException e) {
System.out.println("Cannot addFirst on TreeSet -- order is determined by comparator");
}5. SequencedMap Interface
SequencedMap brings the same concept to maps. It extends Map and provides methods to access the first and last entries, put entries at specific positions, and get sequenced views of keys, values, and entries:
public interface SequencedMap<K, V> extends Map<K, V> {
// Reversed view
SequencedMap<K, V> reversed();
// First and last entries
Map.Entry<K, V> firstEntry();
Map.Entry<K, V> lastEntry();
// Positional put
Map.Entry<K, V> putFirst(K key, V value);
Map.Entry<K, V> putLast(K key, V value);
// Remove from ends
Map.Entry<K, V> pollFirstEntry();
Map.Entry<K, V> pollLastEntry();
// Sequenced views
SequencedSet<K> sequencedKeySet();
SequencedCollection<V> sequencedValues();
SequencedSet<Map.Entry<K, V>> sequencedEntrySet();
}5.1 Using SequencedMap with LinkedHashMap
LinkedHashMap<String, Integer> rankings = new LinkedHashMap<>();
rankings.put("Alice", 95);
rankings.put("Bob", 88);
rankings.put("Charlie", 92);
// Access first and last entries
Map.Entry<String, Integer> first = rankings.firstEntry();
Map.Entry<String, Integer> last = rankings.lastEntry();
System.out.println("First: " + first); // First: Alice=95
System.out.println("Last: " + last); // Last: Charlie=92
// Put at specific positions
rankings.putFirst("Diana", 99); // Diana goes to the front
rankings.putLast("Eve", 85); // Eve goes to the end
System.out.println(rankings);
// {Diana=99, Alice=95, Bob=88, Charlie=92, Eve=85}
// If the key already exists, putFirst/putLast repositions it
rankings.putFirst("Charlie", 97); // Charlie moves to front with new value
System.out.println(rankings);
// {Charlie=97, Diana=99, Alice=95, Bob=88, Eve=85}
// Poll (remove and return) from ends
Map.Entry<String, Integer> polledFirst = rankings.pollFirstEntry();
Map.Entry<String, Integer> polledLast = rankings.pollLastEntry();
System.out.println("Polled first: " + polledFirst); // Charlie=97
System.out.println("Polled last: " + polledLast); // Eve=85
System.out.println(rankings); // {Diana=99, Alice=95, Bob=88}5.2 Sequenced Views
The sequencedKeySet(), sequencedValues(), and sequencedEntrySet() methods return sequenced views that support all the sequenced operations:
LinkedHashMap<String, Double> prices = new LinkedHashMap<>();
prices.put("Apple", 1.50);
prices.put("Banana", 0.75);
prices.put("Cherry", 3.00);
prices.put("Date", 5.50);
// Sequenced key set -- supports getFirst/getLast
SequencedSet<String> keys = prices.sequencedKeySet();
System.out.println("First key: " + keys.getFirst()); // Apple
System.out.println("Last key: " + keys.getLast()); // Date
// Sequenced values -- supports getFirst/getLast
SequencedCollection<Double> values = prices.sequencedValues();
System.out.println("First value: " + values.getFirst()); // 1.5
System.out.println("Last value: " + values.getLast()); // 5.5
// Sequenced entry set
SequencedSet<Map.Entry<String, Double>> entries = prices.sequencedEntrySet();
System.out.println("First entry: " + entries.getFirst()); // Apple=1.5
System.out.println("Last entry: " + entries.getLast()); // Date=5.5
// Iterate in reverse using reversed views
for (String key : keys.reversed()) {
System.out.println(key + " -> " + prices.get(key));
}
// Output: Date -> 5.5, Cherry -> 3.0, Banana -> 0.75, Apple -> 1.55.3 SequencedMap with TreeMap
Just like TreeSet, TreeMap supports most sequenced operations except putFirst() and putLast() (because entry order is determined by key comparison):
TreeMap<String, Integer> sortedScores = new TreeMap<>();
sortedScores.put("Charlie", 92);
sortedScores.put("Alice", 95);
sortedScores.put("Bob", 88);
// Sorted by key (natural order)
System.out.println(sortedScores); // {Alice=95, Bob=88, Charlie=92}
Map.Entry<String, Integer> firstEntry = sortedScores.firstEntry();
Map.Entry<String, Integer> lastEntry = sortedScores.lastEntry();
System.out.println("First: " + firstEntry); // Alice=95
System.out.println("Last: " + lastEntry); // Charlie=92
// Poll operations work
Map.Entry<String, Integer> polled = sortedScores.pollFirstEntry();
System.out.println("Polled: " + polled); // Alice=95
System.out.println(sortedScores); // {Bob=88, Charlie=92}6. Collections Hierarchy Changes
Java 21 retrofits the three new interfaces into the existing collections hierarchy. Here is how the hierarchy looks after Java 21:
6.1 Interface Hierarchy
| New Interface | Extends | Purpose |
|---|---|---|
SequencedCollection |
Collection |
Ordered collection with first/last access |
SequencedSet |
Set, SequencedCollection |
Ordered set with no duplicates |
SequencedMap |
Map |
Ordered map with first/last entry access |
6.2 Which Classes Implement What
| Class | Implements | addFirst/addLast | Notes |
|---|---|---|---|
ArrayList |
SequencedCollection (via List) |
Supported | addFirst is O(n) due to shifting |
LinkedList |
SequencedCollection (via List, Deque) |
Supported | O(1) for both ends |
ArrayDeque |
SequencedCollection (via Deque) |
Supported | O(1) amortized for both ends |
LinkedHashSet |
SequencedSet |
Supported (repositions if exists) | Maintains insertion order |
TreeSet |
SequencedSet (via SortedSet, NavigableSet) |
Throws UnsupportedOperationException | Order determined by comparator |
ConcurrentSkipListSet |
SequencedSet |
Throws UnsupportedOperationException | Concurrent sorted set |
LinkedHashMap |
SequencedMap |
putFirst/putLast supported | Maintains insertion order |
TreeMap |
SequencedMap (via SortedMap, NavigableMap) |
putFirst/putLast throw UnsupportedOperationException | Order determined by key comparator |
ConcurrentSkipListMap |
SequencedMap |
putFirst/putLast throw UnsupportedOperationException | Concurrent sorted map |
6.3 List Now Extends SequencedCollection
The List interface itself now extends SequencedCollection. This means every List implementation automatically inherits getFirst(), getLast(), and all other sequenced methods. The same is true for Deque, SortedSet, and NavigableSet.
// List extends SequencedCollection -- so these methods are available on ALL lists
List<String> immutableList = List.of("one", "two", "three");
System.out.println(immutableList.getFirst()); // one
System.out.println(immutableList.getLast()); // three
// reversed() also works on immutable lists -- returns a view
List<String> reversedView = immutableList.reversed();
System.out.println(reversedView); // [three, two, one]
// The original is unchanged
System.out.println(immutableList); // [one, two, three]
// Note: addFirst/addLast/removeFirst/removeLast throw
// UnsupportedOperationException on immutable lists7. The reversed() View
The reversed() method is one of the most powerful additions. It returns a view of the collection in reverse order — not a copy. This is an important distinction. A view does not allocate new memory for the elements. It simply provides a reversed perspective of the same underlying data. Modifications through the view are reflected in the original, and vice versa.
7.1 reversed() Returns a View, Not a Copy
List<String> original = new ArrayList<>(List.of("A", "B", "C", "D", "E"));
List<String> reversed = original.reversed();
System.out.println("Original: " + original); // [A, B, C, D, E]
System.out.println("Reversed: " + reversed); // [E, D, C, B, A]
// Modify through the reversed view
reversed.addFirst("Z"); // adds to the END of the original
System.out.println("Original: " + original); // [A, B, C, D, E, Z]
System.out.println("Reversed: " + reversed); // [Z, E, D, C, B, A]
// Modify the original -- reflected in the view
original.addFirst("START");
System.out.println("Original: " + original); // [START, A, B, C, D, E, Z]
System.out.println("Reversed: " + reversed); // [Z, E, D, C, B, A, START]7.2 Using reversed() for Iteration
The reversed view makes backward iteration trivial with enhanced for loops and streams:
List<String> history = new ArrayList<>(List.of("page1", "page2", "page3", "page4"));
// Iterate in reverse with enhanced for loop -- clean and readable
System.out.println("Recent history (newest first):");
for (String page : history.reversed()) {
System.out.println(" " + page);
}
// Output:
// page4
// page3
// page2
// page1
// Use with streams
history.reversed().stream()
.limit(3)
.forEach(page -> System.out.println("Recent: " + page));
// Output:
// Recent: page4
// Recent: page3
// Recent: page2
// Works with forEach too
history.reversed().forEach(System.out::println);7.3 reversed() on Maps
The reversed view on a SequencedMap reverses the entry order:
LinkedHashMap<String, Integer> orderedMap = new LinkedHashMap<>();
orderedMap.put("Monday", 1);
orderedMap.put("Tuesday", 2);
orderedMap.put("Wednesday", 3);
orderedMap.put("Thursday", 4);
orderedMap.put("Friday", 5);
// Reversed map view
SequencedMap<String, Integer> reversedMap = orderedMap.reversed();
System.out.println("Original first: " + orderedMap.firstEntry()); // Monday=1
System.out.println("Reversed first: " + reversedMap.firstEntry()); // Friday=5
// Iterate the map in reverse
for (var entry : orderedMap.reversed().entrySet()) {
System.out.println(entry.getKey() + " = " + entry.getValue());
}
// Friday = 5
// Thursday = 4
// Wednesday = 3
// Tuesday = 2
// Monday = 1
// Double reverse returns original order
SequencedMap<String, Integer> doubleReversed = orderedMap.reversed().reversed();
System.out.println(doubleReversed.firstEntry()); // Monday=17.4 Creating an Independent Reversed Copy
If you need a reversed copy that is independent of the original, use the copy constructor or stream().toList():
List<String> original = new ArrayList<>(List.of("A", "B", "C"));
// Independent reversed copy (changes to original do not affect the copy)
List<String> reversedCopy = new ArrayList<>(original.reversed());
// Or with streams
List<String> reversedImmutable = original.reversed().stream().toList();
original.add("D");
System.out.println("Original: " + original); // [A, B, C, D]
System.out.println("Reversed copy: " + reversedCopy); // [C, B, A] (unaffected)
System.out.println("Reversed immutable: " + reversedImmutable); // [C, B, A] (unaffected)8. Practical Examples
Let us look at real-world scenarios where sequenced collections make your code cleaner and more expressive.
8.1 LRU Cache with SequencedMap
A Least Recently Used (LRU) cache evicts the oldest entry when the cache is full. With SequencedMap, this becomes trivial:
import java.util.*;
public class LRUCache<K, V> {
private final int maxSize;
private final LinkedHashMap<K, V> cache;
public LRUCache(int maxSize) {
this.maxSize = maxSize;
// accessOrder=true means most recently accessed entry moves to the end
this.cache = new LinkedHashMap<>(16, 0.75f, true);
}
public V get(K key) {
return cache.get(key); // automatically moves to end (most recent)
}
public void put(K key, V value) {
cache.put(key, value);
// Evict the oldest entry (first entry) if over capacity
while (cache.size() > maxSize) {
Map.Entry<K, V> eldest = cache.pollFirstEntry(); // Java 21!
System.out.println("Evicted: " + eldest);
}
}
public V getMostRecent() {
return cache.isEmpty() ? null : cache.lastEntry().getValue(); // Java 21!
}
public V getLeastRecent() {
return cache.isEmpty() ? null : cache.firstEntry().getValue(); // Java 21!
}
@Override
public String toString() {
return cache.toString();
}
public static void main(String[] args) {
LRUCache<String, String> cache = new LRUCache<>(3);
cache.put("user:1", "Alice");
cache.put("user:2", "Bob");
cache.put("user:3", "Charlie");
System.out.println(cache); // {user:1=Alice, user:2=Bob, user:3=Charlie}
cache.get("user:1"); // access moves user:1 to the end
System.out.println(cache); // {user:2=Bob, user:3=Charlie, user:1=Alice}
cache.put("user:4", "Diana"); // evicts user:2 (least recently used)
System.out.println(cache); // {user:3=Charlie, user:1=Alice, user:4=Diana}
System.out.println("Most recent: " + cache.getMostRecent()); // Diana
System.out.println("Least recent: " + cache.getLeastRecent()); // Charlie
}
}8.2 Browser History / Undo System
A history system where you need to access both the most recent action and the oldest, and iterate in reverse order to show “recent first”:
import java.util.*;
import java.time.LocalDateTime;
public class BrowsingHistory {
private final LinkedHashSet<String> visited = new LinkedHashSet<>();
private final int maxHistory;
public BrowsingHistory(int maxHistory) {
this.maxHistory = maxHistory;
}
public void visit(String url) {
// If already visited, move to the end (most recent)
visited.addLast(url); // repositions if already exists -- Java 21!
// Trim old history
while (visited.size() > maxHistory) {
String oldest = visited.removeFirst(); // Java 21!
System.out.println("Trimmed from history: " + oldest);
}
}
public String currentPage() {
return visited.isEmpty() ? null : visited.getLast(); // Java 21!
}
public String oldestPage() {
return visited.isEmpty() ? null : visited.getFirst(); // Java 21!
}
public List<String> recentHistory(int count) {
// Recent pages first using reversed view -- Java 21!
return visited.reversed().stream()
.limit(count)
.toList();
}
public static void main(String[] args) {
BrowsingHistory history = new BrowsingHistory(5);
history.visit("google.com");
history.visit("stackoverflow.com");
history.visit("github.com");
history.visit("docs.oracle.com");
history.visit("reddit.com");
System.out.println("Current: " + history.currentPage()); // reddit.com
System.out.println("Oldest: " + history.oldestPage()); // google.com
System.out.println("Recent 3: " + history.recentHistory(3));
// [reddit.com, docs.oracle.com, github.com]
// Re-visiting a page moves it to the end
history.visit("google.com");
System.out.println("Current: " + history.currentPage()); // google.com
System.out.println("Recent 3: " + history.recentHistory(3));
// [google.com, reddit.com, docs.oracle.com]
}
}8.3 Task Queue with Priority and Ordering
A task queue where you can add high-priority tasks to the front and normal tasks to the back:
import java.util.*;
public class TaskQueue {
private final List<String> tasks = new ArrayList<>();
public void addTask(String task) {
tasks.addLast(task); // Java 21 -- same as add() but more expressive
}
public void addUrgentTask(String task) {
tasks.addFirst(task); // Java 21 -- urgent tasks go to front
}
public String processNext() {
if (tasks.isEmpty()) return null;
return tasks.removeFirst(); // Java 21 -- process from the front
}
public String peekNext() {
return tasks.isEmpty() ? null : tasks.getFirst(); // Java 21
}
public String peekLast() {
return tasks.isEmpty() ? null : tasks.getLast(); // Java 21
}
public List<String> getAllTasks() {
return Collections.unmodifiableList(tasks);
}
public static void main(String[] args) {
TaskQueue queue = new TaskQueue();
queue.addTask("Write unit tests");
queue.addTask("Update documentation");
queue.addTask("Deploy to staging");
queue.addUrgentTask("Fix production bug"); // goes to front!
System.out.println("All tasks: " + queue.getAllTasks());
// [Fix production bug, Write unit tests, Update documentation, Deploy to staging]
System.out.println("Processing: " + queue.processNext()); // Fix production bug
System.out.println("Processing: " + queue.processNext()); // Write unit tests
}
}8.4 Sorted Leaderboard
A leaderboard that always keeps scores sorted and lets you quickly get the top and bottom players:
import java.util.*;
public class Leaderboard {
// TreeMap sorts by score (descending), then by name
private final TreeMap<Integer, List<String>> scoreBoard = new TreeMap<>(Comparator.reverseOrder());
public void addScore(String player, int score) {
scoreBoard.computeIfAbsent(score, k -> new ArrayList<>()).add(player);
}
public Map.Entry<Integer, List<String>> getTopScore() {
return scoreBoard.firstEntry(); // Java 21 -- highest score (reversed order)
}
public Map.Entry<Integer, List<String>> getLowestScore() {
return scoreBoard.lastEntry(); // Java 21 -- lowest score
}
public void printLeaderboard() {
System.out.println("=== Leaderboard ===");
int rank = 1;
for (var entry : scoreBoard.sequencedEntrySet()) { // Java 21
for (String player : entry.getValue()) {
System.out.printf("#%d %s - %d points%n", rank++, player, entry.getKey());
}
}
}
public void printBottomUp() {
System.out.println("=== Bottom to Top ===");
for (var entry : scoreBoard.reversed().sequencedEntrySet()) { // Java 21
for (String player : entry.getValue()) {
System.out.printf(" %s - %d points%n", player, entry.getKey());
}
}
}
public static void main(String[] args) {
Leaderboard lb = new Leaderboard();
lb.addScore("Alice", 1500);
lb.addScore("Bob", 1200);
lb.addScore("Charlie", 1800);
lb.addScore("Diana", 1500); // same score as Alice
lb.addScore("Eve", 900);
lb.printLeaderboard();
// #1 Charlie - 1800 points
// #2 Alice - 1500 points
// #3 Diana - 1500 points
// #4 Bob - 1200 points
// #5 Eve - 900 points
System.out.println("Top: " + lb.getTopScore()); // 1800=[Charlie]
System.out.println("Bottom: " + lb.getLowestScore()); // 900=[Eve]
}
}9. Comparison Table: Old Way vs New Way
Here is a comprehensive comparison of how common operations looked before Java 21 versus the clean API that sequenced collections provide:
| Operation | Old Way (Pre-Java 21) | New Way (Java 21) |
|---|---|---|
| Get first element of a List | list.get(0) |
list.getFirst() |
| Get last element of a List | list.get(list.size() - 1) |
list.getLast() |
| Get first element of a SortedSet | sortedSet.first() |
sortedSet.getFirst() |
| Get last element of a SortedSet | sortedSet.last() |
sortedSet.getLast() |
| Get first element of a LinkedHashSet | linkedHashSet.iterator().next() |
linkedHashSet.getFirst() |
| Get last element of a LinkedHashSet | Loop through entire set | linkedHashSet.getLast() |
| Remove first element of a List | list.remove(0) |
list.removeFirst() |
| Remove last element of a List | list.remove(list.size() - 1) |
list.removeLast() |
| Add to front of a List | list.add(0, element) |
list.addFirst(element) |
| Reverse iteration of a List | Collections.reverse(copy) or ListIterator |
list.reversed() |
| Get first entry of a LinkedHashMap | map.entrySet().iterator().next() |
map.firstEntry() |
| Get last entry of a LinkedHashMap | Loop through entire entry set | map.lastEntry() |
| Reverse iteration of a Map | Copy keys to list, reverse, iterate | map.reversed().forEach(...) |
| Generic “get first” for any ordered collection | Impossible without instanceof checks | sequencedCollection.getFirst() |
The pattern is clear: the new API is more readable, more consistent, and more composable. You no longer need to remember different method names for conceptually identical operations.
10. Best Practices
10.1 Prefer SequencedCollection as a Parameter Type
When writing methods that need ordered access, use SequencedCollection as the parameter type instead of concrete types. This makes your methods work with lists, deques, and ordered sets:
// Good -- accepts any sequenced collection
public static <E> E getLastOrDefault(SequencedCollection<E> collection, E defaultValue) {
return collection.isEmpty() ? defaultValue : collection.getLast();
}
// Good -- works with SequencedMap
public static <K, V> V getNewestValue(SequencedMap<K, V> map) {
Map.Entry<K, V> last = map.lastEntry();
return last == null ? null : last.getValue();
}
// Avoid -- too specific
public static String getLastElement(ArrayList<String> list) {
return list.get(list.size() - 1);
}10.2 Be Aware of Performance Characteristics
Not all sequenced operations are O(1) for every collection type:
| Operation | ArrayList | LinkedList | ArrayDeque | LinkedHashSet | TreeSet |
|---|---|---|---|---|---|
getFirst() |
O(1) | O(1) | O(1) | O(1) | O(log n) |
getLast() |
O(1) | O(1) | O(1) | O(1) | O(log n) |
addFirst() |
O(n) | O(1) | O(1) | O(1) | N/A |
addLast() |
O(1)* | O(1) | O(1)* | O(1) | N/A |
removeFirst() |
O(n) | O(1) | O(1) | O(1) | O(log n) |
removeLast() |
O(1) | O(1) | O(1) | O(1) | O(log n) |
reversed() |
O(1) | O(1) | O(1) | O(1) | O(1) |
* Amortized O(1). Key takeaway: addFirst() and removeFirst() on ArrayList are O(n) because all elements must be shifted. If you frequently add or remove from the front, use ArrayDeque or LinkedList instead.
10.3 Use reversed() Instead of Collections.reverse()
Collections.reverse() mutates the list in place. reversed() returns a lightweight view with zero allocation overhead. Prefer reversed() unless you specifically need to reorder the underlying data:
List<String> logs = getRecentLogs();
// Bad -- mutates the list, allocates nothing but changes state
Collections.reverse(logs);
for (String log : logs) {
process(log);
}
Collections.reverse(logs); // have to reverse back!
// Good -- view-based, no mutation, no allocation
for (String log : logs.reversed()) {
process(log);
}10.4 Handle Empty Collections
getFirst(), getLast(), removeFirst(), and removeLast() throw NoSuchElementException on empty collections. Always check for emptiness first, or use a try-catch if the empty case is exceptional:
// Safe access pattern
public static <E> Optional<E> safeGetFirst(SequencedCollection<E> collection) {
return collection.isEmpty() ? Optional.empty() : Optional.of(collection.getFirst());
}
public static <E> Optional<E> safeGetLast(SequencedCollection<E> collection) {
return collection.isEmpty() ? Optional.empty() : Optional.of(collection.getLast());
}
// Usage
List<String> items = fetchItems();
String first = safeGetFirst(items).orElse("No items");
String last = safeGetLast(items).orElse("No items");10.5 Remember That reversed() Views Are Live
Since reversed() returns a view, be careful not to modify the original collection while iterating over its reversed view (unless you specifically want to). This follows the same concurrent modification rules as other collection views:
List<String> names = new ArrayList<>(List.of("Alice", "Bob", "Charlie"));
// This will throw ConcurrentModificationException
try {
for (String name : names.reversed()) {
if (name.startsWith("B")) {
names.remove(name); // modifying original while iterating view!
}
}
} catch (ConcurrentModificationException e) {
System.out.println("Cannot modify during iteration");
}
// Safe approach: collect items to remove first
List<String> toRemove = names.reversed().stream()
.filter(n -> n.startsWith("B"))
.toList();
names.removeAll(toRemove);10.6 Migrate Gradually
You do not need to rewrite all your code at once. Here is a prioritized migration approach:
- Replace
list.get(0)withlist.getFirst()— immediate readability improvement, zero risk - Replace
list.get(list.size() - 1)withlist.getLast()— eliminates off-by-one risk - Replace
Collections.reverse()withreversed()— when you only need reversed iteration, not mutation - Update method signatures to use
SequencedCollection— when refactoring utility methods - Use
firstEntry()/lastEntry()on maps — when working with LinkedHashMap or TreeMap
Sequenced Collections is one of those features that seems small on the surface but fundamentally improves the Java Collections Framework. The uniform API, the lightweight reversed views, and the ability to write truly generic collection-handling code make this one of the most practical additions in Java 21. Start using getFirst(), getLast(), and reversed() today — your code will be cleaner for it.