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.
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.
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 Listlist = List.of("alpha", "beta", "gamma"); String first = list.get(0); // Deque -- dedicated method Deque deque = new ArrayDeque<>(List.of("alpha", "beta", "gamma")); String first = deque.getFirst(); // SortedSet -- yet another method SortedSet sortedSet = new TreeSet<>(List.of("alpha", "beta", "gamma")); String first = sortedSet.first(); // LinkedHashSet -- no direct method at all! LinkedHashSet linkedHashSet = new LinkedHashSet<>(List.of("alpha", "beta", "gamma")); String first = linkedHashSet.iterator().next(); // ugly
Getting the last element was even worse:
// Getting the last element -- even more inconsistent // List -- calculate size minus one Listlist = List.of("alpha", "beta", "gamma"); String last = list.get(list.size() - 1); // Deque -- dedicated method Deque deque = new ArrayDeque<>(List.of("alpha", "beta", "gamma")); String last = deque.getLast(); // SortedSet -- different name than Deque SortedSet sortedSet = new TreeSet<>(List.of("alpha", "beta", "gamma")); String last = sortedSet.last(); // LinkedHashSet -- absolutely terrible LinkedHashSet 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 }
Reversing the iteration order was equally inconsistent:
// Reversing -- no common approach // List -- create a new reversed copy Listlist = new ArrayList<>(List.of("alpha", "beta", "gamma")); Collections.reverse(list); // mutates the original! // or use ListIterator to go backwards (verbose) ListIterator it = list.listIterator(list.size()); while (it.hasPrevious()) { System.out.println(it.previous()); } // Deque -- use descendingIterator Deque deque = new ArrayDeque<>(List.of("alpha", "beta", "gamma")); Iterator descIt = deque.descendingIterator(); // NavigableSet -- descendingSet returns a view NavigableSet navSet = new TreeSet<>(List.of("alpha", "beta", "gamma")); NavigableSet 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 LinkedHashSet
This 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.
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 SequencedCollectionextends Collection { // Returns a reversed-order view of this collection SequencedCollection reversed(); // First element operations void addFirst(E e); void addLast(E e); E getFirst(); E getLast(); E removeFirst(); E removeLast(); }
Now every ordered collection speaks the same language:
import java.util.*;
public class SequencedCollectionDemo {
public static void main(String[] args) {
// ArrayList implements SequencedCollection
List list = new ArrayList<>(List.of("alpha", "beta", "gamma"));
System.out.println(list.getFirst()); // alpha
System.out.println(list.getLast()); // gamma
// ArrayDeque implements SequencedCollection
Deque 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 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 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.
These methods add elements at the beginning or end of the collection:
Listlanguages = 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 stack = new ArrayDeque<>(); stack.addFirst("bottom"); stack.addFirst("middle"); stack.addFirst("top"); System.out.println(stack); // [top, middle, bottom]
These methods remove and return elements from the ends:
Listtasks = 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 empty = new ArrayList<>(); try { empty.getFirst(); // NoSuchElementException } catch (NoSuchElementException e) { System.out.println("Collection is empty: " + e.getMessage()); }
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 staticvoid printEndpoints(SequencedCollection 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.
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 SequencedSetextends Set , SequencedCollection { @Override SequencedSet 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 LinkedHashSetcolors = 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]
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:
TreeSetscores = 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"); }
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 SequencedMapextends Map { // Reversed view SequencedMap reversed(); // First and last entries Map.Entry firstEntry(); Map.Entry lastEntry(); // Positional put Map.Entry putFirst(K key, V value); Map.Entry putLast(K key, V value); // Remove from ends Map.Entry pollFirstEntry(); Map.Entry pollLastEntry(); // Sequenced views SequencedSet sequencedKeySet(); SequencedCollection sequencedValues(); SequencedSet > sequencedEntrySet(); }
LinkedHashMaprankings = new LinkedHashMap<>(); rankings.put("Alice", 95); rankings.put("Bob", 88); rankings.put("Charlie", 92); // Access first and last entries Map.Entry first = rankings.firstEntry(); Map.Entry 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 polledFirst = rankings.pollFirstEntry(); Map.Entry 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}
The sequencedKeySet(), sequencedValues(), and sequencedEntrySet() methods return sequenced views that support all the sequenced operations:
LinkedHashMapprices = 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 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 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 > 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.5
Just like TreeSet, TreeMap supports most sequenced operations except putFirst() and putLast() (because entry order is determined by key comparison):
TreeMapsortedScores = 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 firstEntry = sortedScores.firstEntry(); Map.Entry lastEntry = sortedScores.lastEntry(); System.out.println("First: " + firstEntry); // Alice=95 System.out.println("Last: " + lastEntry); // Charlie=92 // Poll operations work Map.Entry polled = sortedScores.pollFirstEntry(); System.out.println("Polled: " + polled); // Alice=95 System.out.println(sortedScores); // {Bob=88, Charlie=92}
Java 21 retrofits the three new interfaces into the existing collections hierarchy. Here is how the hierarchy looks after Java 21:
| 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 |
| 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 |
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 ListimmutableList = 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 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 lists
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.
Listoriginal = new ArrayList<>(List.of("A", "B", "C", "D", "E")); List 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]
The reversed view makes backward iteration trivial with enhanced for loops and streams:
Listhistory = 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);
The reversed view on a SequencedMap reverses the entry order:
LinkedHashMaporderedMap = 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 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 doubleReversed = orderedMap.reversed().reversed(); System.out.println(doubleReversed.firstEntry()); // Monday=1
If you need a reversed copy that is independent of the original, use the copy constructor or stream().toList():
Listoriginal = new ArrayList<>(List.of("A", "B", "C")); // Independent reversed copy (changes to original do not affect the copy) List reversedCopy = new ArrayList<>(original.reversed()); // Or with streams List 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)
Let us look at real-world scenarios where sequenced collections make your code cleaner and more expressive.
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{ private final int maxSize; private final LinkedHashMap 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 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 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 } }
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 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 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]
}
}
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 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 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
}
}
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> scoreBoard = new TreeMap<>(Comparator.reverseOrder());
public void addScore(String player, int score) {
scoreBoard.computeIfAbsent(score, k -> new ArrayList<>()).add(player);
}
public Map.Entry> getTopScore() {
return scoreBoard.firstEntry(); // Java 21 -- highest score (reversed order)
}
public Map.Entry> 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]
}
}
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.
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 staticE getLastOrDefault(SequencedCollection collection, E defaultValue) { return collection.isEmpty() ? defaultValue : collection.getLast(); } // Good -- works with SequencedMap public static V getNewestValue(SequencedMap map) { Map.Entry last = map.lastEntry(); return last == null ? null : last.getValue(); } // Avoid -- too specific public static String getLastElement(ArrayList list) { return list.get(list.size() - 1); }
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.
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:
Listlogs = 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); }
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 staticOptional safeGetFirst(SequencedCollection collection) { return collection.isEmpty() ? Optional.empty() : Optional.of(collection.getFirst()); } public static Optional safeGetLast(SequencedCollection collection) { return collection.isEmpty() ? Optional.empty() : Optional.of(collection.getLast()); } // Usage List items = fetchItems(); String first = safeGetFirst(items).orElse("No items"); String last = safeGetLast(items).orElse("No items");
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:
Listnames = 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 toRemove = names.reversed().stream() .filter(n -> n.startsWith("B")) .toList(); names.removeAll(toRemove);
You do not need to rewrite all your code at once. Here is a prioritized migration approach:
list.get(0) with list.getFirst() — immediate readability improvement, zero risklist.get(list.size() - 1) with list.getLast() — eliminates off-by-one riskCollections.reverse() with reversed() — when you only need reversed iteration, not mutationSequencedCollection — when refactoring utility methodsfirstEntry()/lastEntry() on maps — when working with LinkedHashMap or TreeMapSequenced 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.