☕ Java

Sequenced Collections

Sequenced Collections, finalized in Java 21 (JEP 431), introduce three new interfaces — SequencedCollection, SequencedSet, and SequencedMap — that retrofit a uniform contract for 'has a defined encounter order, with a first element and a last element' onto the collections framework, providing consistent getFirst()/getLast()/addFirst()/addLast()/removeFirst()/removeLast() methods and a reversed() view across List, Deque, LinkedHashSet, and LinkedHashMap, all of which already had this ordering property informally but exposed it through inconsistent, type-specific APIs (or, for LinkedHashSet and LinkedHashMap, didn't expose it as a usable API at all). This entry covers exactly what inconsistency existed before Java 21 and why it mattered, the three new interfaces and the methods they add, the reversed() view and how it differs from creating a genuinely new reversed copy, and which existing collection types were retrofitted to implement these interfaces.

Motivation — Encounter Order Was a Real Property With No Uniform API

Several widely-used collection types in the Java collections framework have always had a well-defined encounter order — a genuine first element and last element, and a consistent iteration sequence between them — but the framework never provided a single, uniform interface capturing that shared property, leaving each type to expose (or fail to expose) it through its own separate, inconsistent API. List has always had a defined order and offered get(0) and get(size()-1) for first/last access, but no dedicated getFirst()/getLast() methods, and no built-in way to obtain a reversed view without manually calling Collections.reverse(...) (which mutates the list in place) or constructing a separate reversed copy. Deque (and its implementations like ArrayDeque and LinkedList) has always supported getFirst()/getLast()/addFirst()/addLast()/removeFirst()/removeLast() directly, since a double-ended queue's whole purpose centers on its two ends — but this rich API existed only on Deque, with no relationship enforced or even expressible between Deque's ordering operations and List's, despite both fundamentally representing the same "ordered sequence with a first and last" concept. LinkedHashSet and LinkedHashMap were the most underserved: both maintain a well-defined, predictable insertion order (unlike HashSet/HashMap, whose iteration order is unspecified and can vary even between runs), but neither exposed any direct way to get the first or last element, or to add at a specific end, or to iterate in reverse order — despite genuinely having that ordering property internally, a caller wanting the first-inserted entry of a LinkedHashMap had no choice but to call iterator().next() and discard everything else, or convert the whole map to a List of entries first, simply to express an operation ("give me the first element of this ordered collection") that should have been a single direct method call. Sequenced Collections, introduced via JEP 431 in Java 21, address this by defining the shared "has a defined encounter order" property as an explicit, named contract — three new interfaces in the java.util collections hierarchy — and retrofitting every existing type that genuinely has this property to implement the corresponding new interface, giving all of them the exact same uniform set of first/last access, first/last mutation, and reversed-view methods, regardless of which specific concrete type is being used.
Java
// ── Before Java 21 — inconsistent, type-specific access to the same property
List<String> list = new ArrayList<>(List.of("a", "b", "c"));
String firstOld = list.get(0);                 // List — no getFirst(), use get(0)
String lastOld = list.get(list.size() - 1);     // List — no getLast(), manual index arithmetic

Deque<String> deque = new ArrayDeque<>(List.of("a", "b", "c"));
String firstDeque = deque.getFirst();           // Deque — HAS getFirst()/getLast() directly
String lastDeque = deque.getLast();

LinkedHashMap<String, Integer> lhm = new LinkedHashMap<>();
lhm.put("a", 1); lhm.put("b", 2); lhm.put("c", 3);
// No direct way at all to get the first-inserted entry before Java 21:
Map.Entry<String, Integer> firstEntryOld = lhm.entrySet().iterator().next();   // awkward workaround
// Getting it in REVERSE order required converting to a List first, or similar workarounds

// ── Java 21+ — uniform SequencedCollection/SequencedMap API everywhere ─
String firstNew = list.getFirst();              // List now HAS getFirst()/getLast() directly
String lastNew = list.getLast();

Map.Entry<String, Integer> firstEntryNew = lhm.firstEntry();   // direct, uniform method
Map.Entry<String, Integer> lastEntryNew = lhm.lastEntry();

The Three New Interfaces and Their Methods

SequencedCollection<E> extends Collection<E> and adds: addFirst(E), addLast(E), getFirst(), getLast(), removeFirst(), removeLast(), and reversed() (covered in detail in the next section). Some of these methods (addFirst/addLast/removeFirst/removeLast) throw UnsupportedOperationException on implementations that are fixed-size or immutable, exactly the same way add()/remove() already do on those implementations — SequencedCollection doesn't change any existing mutability guarantees, it simply gives a uniform method signature to an operation that may or may not be supported, consistent with how the rest of the collections framework already handles optional operations. List now extends SequencedCollection directly, immediately gaining all of these methods without any implementation needing to change, since List's existing get(0)/get(size()-1)-based behavior already satisfies the new interface's contract. Deque also now extends SequencedCollection, unifying it with List under the same shared ordering contract for the first time, even though the two had always conceptually shared this property. SequencedSet<E> extends SequencedCollection<E> and Set<E>, adding nothing beyond what SequencedCollection already provides except the combined Set contract (uniqueness) layered on top — it exists specifically so that LinkedHashSet (which is both a Set and has a defined encounter order) has a precise interface to implement, and so that reversed() on a sequenced set can be correctly typed to return another SequencedSet rather than a plain SequencedCollection. LinkedHashSet now implements SequencedSet, immediately gaining getFirst(), getLast(), addFirst(), addLast(), removeFirst(), removeLast(), and reversed() — none of which it had any direct way to express before Java 21. SequencedMap<K,V> extends Map<K,V> and adds a parallel set of methods adapted for map semantics: putFirst(K,V) and putLast(K,V) (inserting or re-positioning an entry at a given end), firstEntry() and lastEntry() (returning the first/last Map.Entry, or null if the map is empty, mirroring the null-on-empty convention NavigableMap already established rather than throwing), pollFirstEntry() and pollLastEntry() (removing and returning the first/last entry), and reversed() returning a reverse-ordered SequencedMap view. LinkedHashMap now implements SequencedMap, and SortedMap (and its NavigableMap sub-interface, implemented by TreeMap) also now extends SequencedMap, unifying TreeMap's already-existing firstKey()/lastKey()-style navigation methods under the same broader contract.
Java
// ── SequencedCollection — List and Deque both implement it ─────────────
List<String> names = new ArrayList<>(List.of("Ann", "Bo", "Cy"));
names.addFirst("Aaa");          // inserts at the very front
names.addLast("Zz");            // inserts at the very end
System.out.println(names);      // [Aaa, Ann, Bo, Cy, Zz]
String first = names.getFirst(); // "Aaa"
String removed = names.removeLast();   // removes and returns "Zz"

Deque<String> dq = new ArrayDeque<>(List.of("x", "y", "z"));
// Deque already had these methods — now formally unified under SequencedCollection too
dq.addFirst("w");
dq.getLast();

// ── Immutable/fixed-size collections still throw for unsupported mutation
List<String> immutable = List.of("a", "b", "c");
// immutable.addFirst("z");   // UnsupportedOperationException — same as add() already would

// ── SequencedSet — LinkedHashSet finally gets first/last access ────────
LinkedHashSet<String> lhs = new LinkedHashSet<>(List.of("a", "b", "c"));
lhs.addLast("d");                  // adds "d" at the end (re-positions if already present)
String firstSet = lhs.getFirst();   // "a" — direct access, no iterator() workaround needed
lhs.removeFirst();                  // removes "a"

// ── SequencedMap — LinkedHashMap and TreeMap both implement it ─────────
LinkedHashMap<String, Integer> lhm = new LinkedHashMap<>();
lhm.put("a", 1); lhm.put("b", 2); lhm.put("c", 3);

Map.Entry<String, Integer> firstEntry = lhm.firstEntry();   // a=1, no manual iterator needed
Map.Entry<String, Integer> lastEntry = lhm.lastEntry();      // c=3
lhm.putFirst("zero", 0);        // inserts/repositions at the very front of the encounter order
Map.Entry<String, Integer> polled = lhm.pollFirstEntry();    // removes and returns the first entry

TreeMap<String, Integer> tm = new TreeMap<>(lhm);
tm.firstEntry();   // TreeMap already had firstKey()/lastKey()-style methods —
                     // now formally unified under the same SequencedMap contract

The reversed() View — A Live, Backed Reversal, Not a Copy

reversed() is the method common to all three new interfaces, and its defining characteristic is that it does not produce an independent copy of the collection with elements in reverse order — it returns a view, backed by the original collection, that presents the same elements in reverse encounter order, where structural changes made through the original collection are immediately visible through the reversed view, and (for mutable collections) structural changes made through the reversed view are written back to the original collection. This mirrors the existing "view" pattern the collections framework already uses elsewhere (subList(...), Collections.unmodifiableList(...), keySet()/values()/entrySet() on Map), rather than introducing a new, different kind of relationship between a collection and a derived collection. This live-view behavior is the precise reason reversed() is a meaningfully different (and more useful) operation than the older idiom of calling Collections.reverse(someList), which mutates the original list in place and returns nothing, forcing a choice between losing the original order entirely or first making a defensive copy purely to reverse that copy instead — reversed() requires neither: the original collection's order is completely undisturbed, and the reversed view is available immediately, alongside the original, without any separate copying step. A practical consequence of reversed() returning a live, writable view (rather than a static snapshot) is that iterating over a reversed() view while concurrently structurally modifying the original collection through a different reference triggers the same ConcurrentModificationException behavior that iterating over the original collection directly while modifying it would — the view doesn't introduce new concurrent-modification risk, but it also doesn't shield against the existing risk that already applies to the underlying collection, since the view and the original are, by design, two windows onto the exact same underlying data.
Java
// ── reversed() — a LIVE view, not an independent copy ──────────────────
List<String> original = new ArrayList<>(List.of("a", "b", "c"));
List<String> reversedView = original.reversed();
System.out.println(reversedView);   // [c, b, a]

original.add("d");                   // mutate the ORIGINAL list
System.out.println(reversedView);   // [d, c, b, a] — the view reflects the change immediately,
                                       // because it's backed by "original", not a separate copy

reversedView.addFirst("z");          // mutate THROUGH the reversed view
System.out.println(original);        // [a, b, c, d, z] — "z" appears at original's LOGICAL end,
                                       // since "addFirst" on the reversed view == "addLast" on original

// ── Contrast with the old idiom — Collections.reverse() mutates in place
List<String> oldList = new ArrayList<>(List.of("a", "b", "c"));
Collections.reverse(oldList);        // mutates oldList ITSELF — original order is now GONE
System.out.println(oldList);         // [c, b, a] — no way to recover the original order from here
                                       // without having made a defensive copy beforehand

// To get BOTH the original order AND a reversed view with the old approach,
// a defensive copy was required first:
List<String> copyForReversal = new ArrayList<>(oldList);
Collections.reverse(copyForReversal);   // now two separate lists to manage

// ── Concurrent modification risk is shared between view and original ───
List<String> data = new ArrayList<>(List.of("a", "b", "c"));
List<String> rev = data.reversed();
// for (String s : rev) { data.add("x"); }   // ConcurrentModificationException —
// same risk as modifying "data" while iterating it directly; the view doesn't add or remove this risk

Related Topics in Java 9 to 21 Features

Module System (JPMS)
The Java Platform Module System (JPMS), introduced in Java 9 under Project Jigsaw, adds a module as a new, higher-level unit of code organization above the package, allowing a JAR-like artifact to explicitly declare which packages it exports for use by other modules, which modules it depends on, and which packages it keeps entirely internal and inaccessible from outside — closing a long-standing gap in the classpath model where every public class in every JAR on the classpath was accessible to every other JAR, regardless of whether that access was intended. JPMS was also the mechanism used to decompose the JDK itself, which had grown into one monolithic rt.jar, into roughly 90 separate modules, enabling smaller custom runtime images and explicit internal-API encapsulation (notably restricting access to sun.* and other internal packages that many libraries had relied on despite warnings). This entry covers the classpath problems JPMS solves, module-info.java syntax and the requires/exports/opens directives, the distinction between the classpath and module path including automatic and unnamed modules, and jlink custom runtime images.
JShell
JShell, introduced in Java 9, is an interactive Read-Eval-Print Loop (REPL) bundled directly with the JDK, allowing Java statements, expressions, method, class, and variable declarations to be entered and evaluated one at a time at an interactive prompt, with immediate feedback — without requiring a class wrapper, a main method, explicit compilation, or a build tool, all of which Java had always required for even the smallest snippet of executable code before Java 9. This solved a long-standing friction point for learning, quick experimentation, and API exploration, where languages like Python or Ruby offered an immediate interactive prompt but Java required the full edit-compile-run cycle even to test a single line. This entry covers what problem JShell solves and how it differs from a script, core REPL usage including implicit variable creation and forward references, snippet management commands, and how to feed JShell external classes and files.
Collection Factory Methods
Collection factory methods — List.of(...), Set.of(...), and Map.of(...)/Map.ofEntries(...) — introduced in Java 9, provide a concise, standard way to create small, fixed-content, genuinely immutable collections in a single expression, replacing the previously common but verbose combination of Arrays.asList(...), Collections.unmodifiableList(...), and manual add() calls, none of which produced a collection that was both concise to write and fully immutable. This entry covers exactly what problem these factories solve compared to the older idioms, the specific immutability and null-rejection guarantees they provide (which are stricter than Collections.unmodifiableXxx), their behavior around duplicate keys/elements, and important caveats around mutability expectations and performance for very small collections.
Private Interface Methods
Private interface methods, introduced in Java 9, allow an interface to declare methods marked private — with a full method body — that are accessible only from within that same interface (from its default methods, its other private methods, and its private static methods), but completely invisible to implementing classes and to any code outside the interface. They close a code-duplication gap left after Java 8's default methods: when an interface has multiple default methods that share common logic, that shared logic previously either had to be duplicated in each default method or exposed as a public default method purely to enable reuse, polluting the interface's actual API surface with a method meant only for internal reuse. This entry covers the specific problem they solve, the distinction between private instance and private static interface methods, the rules governing what they can call and be called from, and how they interact with the broader default-method design Java established in version 8.