Java 21 Sequenced Collections

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 list = 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

2.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 list = 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
}

2.3 Reversing a Collection

Reversing the iteration order was equally inconsistent:

// Reversing -- no common approach

// List -- create a new reversed copy
List list = 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.

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 extends 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();
}

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 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.

3.2 addFirst() and addLast()

These methods add elements at the beginning or end of the collection:

List 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 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 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 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  void 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.

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 extends 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
LinkedHashSet 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 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 extends 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();
}

5.1 Using SequencedMap with LinkedHashMap

LinkedHashMap rankings = 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}

5.2 Sequenced Views

The sequencedKeySet(), sequencedValues(), and sequencedEntrySet() methods return sequenced views that support all the sequenced operations:

LinkedHashMap 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 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

5.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 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 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}

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 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 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

7. 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 original = 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]

7.2 Using reversed() for Iteration

The reversed view makes backward iteration trivial with enhanced for loops and streams:

List 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 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 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

7.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 original = 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)

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 {
    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
    }
}

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 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]
    }
}

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 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
    }
}

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> 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]
    }
}

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 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);
}

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 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  Optional 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");

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 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 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:

  1. Replace list.get(0) with list.getFirst() — immediate readability improvement, zero risk
  2. Replace list.get(list.size() - 1) with list.getLast() — eliminates off-by-one risk
  3. Replace Collections.reverse() with reversed() — when you only need reversed iteration, not mutation
  4. Update method signatures to use SequencedCollection — when refactoring utility methods
  5. 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.




Subscribe To Our Newsletter
You will receive our latest post and tutorial.
Thank you for subscribing!

required
required


Leave a Reply

Your email address will not be published. Required fields are marked *