Sequenced collections and what is new

Sequenced collections, unnamed variables, string templates that came and went, and how to read a JEP.

9 min read Modern Java: 8 to 25

Java releases every six months, and the changes that matter to a working engineer arrive in two kinds: a language feature that reshapes how code is written, and a library seam that removes a wart everyone had stopped noticing. Sequenced collections (Java 21) are the second kind, a missing interface that made "the first element" a different method on every collection type. Java 22 through 25 add a handful of the first kind: unnamed variables, flexible constructor bodies, gatherers, scoped values, module imports. This lesson covers each briefly, explains how a new interface was retrofitted onto a twenty-five-year-old hierarchy without breaking anyone, and ends with how to read a release so the next one is not a surprise.

Sequenced collections (Java 21, JEP 431)

Before 21, "get the first element" had no common spelling: list.get(0), deque.getFirst(), sortedSet.first(), linkedHashSet.iterator().next(), and for LinkedHashMap a loop. "Get the last" was worse (list.get(list.size() - 1), linkedHashSet had no answer at all short of iterating). Three new interfaces fix it:

java
interface SequencedCollection<E> extends Collection<E> {
    SequencedCollection<E> reversed();
    void addFirst(E e);   void addLast(E e);
    E getFirst();         E getLast();
    E removeFirst();      E removeLast();
}
interface SequencedSet<E> extends SequencedCollection<E>, Set<E> { SequencedSet<E> reversed(); }
interface SequencedMap<K,V> extends Map<K,V> {
    SequencedMap<K,V> reversed();
    Map.Entry<K,V> firstEntry();    Map.Entry<K,V> lastEntry();
    Map.Entry<K,V> pollFirstEntry(); Map.Entry<K,V> pollLastEntry();
    V putFirst(K k, V v);           V putLast(K k, V v);
    SequencedSet<K> sequencedKeySet(); SequencedCollection<V> sequencedValues(); SequencedSet<Map.Entry<K,V>> sequencedEntrySet();
}

List, Deque, LinkedHashSet, SortedSet, LinkedHashMap and SortedMap implement them. HashSet and HashMap do not: they have no order to expose.

java
List<String> log = ...;
String latest = log.getLast();                                   // was log.get(log.size() - 1)
for (String s : log.reversed()) { ... }                          // was a loop with an index
LinkedHashMap<String, Integer> lru = ...;
lru.putFirst("x", 1);                                            // move-to-front, new
var newestFirst = lru.reversed();                                // a live view

reversed() is a view, not a copy: writes go through, and it is O(1) to create.

Under the hood: retrofitting an interface without breaking the world

Adding an interface to List in 2023 sounds like a compatibility bomb: every List implementation in every jar would have to implement getFirst and friends. It was possible because of two things. Default methods (Java 8): every new method on SequencedCollection has a default implementation in terms of the existing interface, getFirst() is iterator().next(), reversed() on a List is a ReverseOrderListView built on listIterator, so a List compiled in 2005 gains them without recompiling. And method resolution: Deque already had getFirst, addFirst and so on with matching signatures, so nothing clashed; the only conflict was SortedSet, whose existing first() and last() throw NoSuchElementException on empty, the same as the new getFirst and getLast, which was made consistent by design.

Two things did break, and were documented. Classes that already declared a reversed() method with a different return type, which turned out to be almost none in the ecosystem. And any code calling getFirst() on a LinkedHashSet through reflection or a raw type where a subclass had its own getFirst with a different meaning; in practice the JDK team scanned Maven Central and found a handful of cases, an exercise that is now part of every collections change. The lesson for API design: a default method is how a widely implemented interface grows, and its default must be correct, if slow, for every existing implementation.

ReverseOrderListView and its cousins are worth reading once: fifty lines each, implementing the whole collection contract by delegating to the original with indices flipped, which is why reversed() costs nothing and why mutating the view mutates the source.

Unnamed variables and patterns (Java 22, JEP 456)

_ is a variable you do not name:

java
try { Integer.parseInt(s); } catch (NumberFormatException _) { return DEFAULT; }
for (var _ : items) count++;
case Point(var x, _) -> x;
BiFunction<A, B, C> f = (a, _) -> g(a);

The compiler allocates nothing for it and never lets you read it. Use it where the name would only be noise.

Flexible constructor bodies (Java 25, JEP 513)

Statements before super(...) or this(...), as long as they do not touch this:

java
public Order(String rawTotal) {
    long cents = Money.parse(rawTotal);          // validate and compute first
    if (cents < 0) throw new IllegalArgumentException("negative");
    super(cents);                                // then call the superclass
}

It ends the static-helper-method workaround (super(parseAndValidate(x))), and, since Java 25, allows initialising this class's own fields before super() too, which closes an old bug: a superclass constructor calling an overridden method sees the subclass's fields already set instead of null.

Stream gatherers (Java 24, JEP 485)

Custom intermediate operations, covered in the streams lesson: Gatherers.windowFixed, windowSliding, fold, scan, mapConcurrent, and Gatherer.of(...) for your own.

Scoped values and structured concurrency (Java 25, JEP 506 and 505)

ScopedValue is the immutable, bounded successor to ThreadLocal for request context; StructuredTaskScope runs a set of subtasks as one unit with shared cancellation. Both are covered in the virtual-threads lesson. Together with virtual threads, they are the concurrency model for new services.

Module import declarations (Java 25, JEP 511)

java
import module java.base;      // every package exported by java.base, one line

Intended for scripts and small programs, alongside compact source files and instance main methods (JEP 512): a file containing void main() { IO.println("hi"); } runs with java Hello.java. Neither belongs in a service codebase; both make the language teachable and shell-scriptable.

Primitive types in patterns (preview)

JEP 455 and its successors allow case int i and instanceof with primitives, including exactness checks (long l matching an int only when the value fits). Still preview in Java 25; watch for it.

String templates: the feature that went away

STR."Hello \{name}" was preview in Java 21 and 22 and was withdrawn in Java 23 (JEP 465 was not delivered) for redesign. If you see it in a tutorial, it does not compile. String.format, formatted() and text blocks remain the tools.

Walkthrough: the "latest event" that was quadratic

An audit service kept events in a LinkedHashSet<Event> (ordered, deduplicated) and needed the most recent one on each request:

AuditView.java (Java 17)java
Event latest() {
    Event last = null;
    for (Event e : events) last = e;      // O(n): iterate to the end
    return last;
}
  1. The set grew to about 200,000 events per tenant; every request walked all of them to find the last. At 500 requests per second, that was 100 million iterator steps a second on one core, and the service's CPU was pinned at 100% doing nothing.
  2. The alternative on Java 17 was to keep a separate "last" reference, updated on every insert, with the removal path having to recompute it, which was where two bugs had already lived.
  3. On Java 21, LinkedHashSet is a SequencedSet: events.getLast() is O(1), reading the tail of the internal doubly linked list that had been there since Java 1.4 without a public accessor.
  4. The change was one line; CPU fell to 3%. The "newest first" endpoint became events.reversed().stream().limit(20), a view, with no copy.
  5. A trap found in review: events.reversed().add(x) inserts at the front of the original, which is addFirst. Views mirror; they do not just read.

The general shape: an ordered structure had the operation internally and lacked the method, and the retrofit exposed it. Before writing a helper, check whether the JDK version you run on already has the seam.

How to read a release

Each JDK release page lists JEPs. Sort them into three piles:

  1. Final language features: read the JEP, they change what code reviews accept.
  2. Final library features: skim; check whether they retire a helper or a dependency you carry.
  3. Preview and incubator: note the names, do not use them in production, expect them to change (string templates did).

Then the release notes' "Removed features and options" section, which is where a -XX flag you rely on, SecurityManager (removed in Java 24), or an old GC quietly goes.

The upgrade cadence that works for services: move between LTS releases (17 → 21 → 25), and test each intermediate release in CI so the LTS jump is small. Java 25 is the current LTS.

Try it yourself

Which compile on Java 21?

(a) new HashSet<>(List.of(1, 2)).getFirst() (b) new TreeSet<>(List.of(2, 1)).getLast() (c) List.of(1, 2).reversed().add(3) (d) new ArrayDeque<>().getFirst() (e) new LinkedHashMap<String,Integer>().putFirst("a", 1)

Answer

(a) does not compile: HashSet is not sequenced. (b) compiles, returns 2. (c) compiles and throws UnsupportedOperationException at run time: List.of is immutable, and the reversed view forwards the write. (d) compiles and throws NoSuchElementException, empty. (e) compiles and inserts at the front. Compile-time is about whether the type has an order; run-time is about whether it is mutable and non-empty.

Live view or copy?

java
var list = new ArrayList<>(List.of("a", "b", "c"));
var rev = list.reversed();
list.add("d");
rev.removeFirst();
System.out.println(list + " " + rev);
Answer

[a, b, c] [c, b, a]. rev is a view: after add("d") it reads [d, c, b, a], and removeFirst() on the view removes d from the end of list. Both then print the same three elements in opposite orders. Nothing was copied, and every mutation went through.

Why did the retrofit not break the world?

List gained six new methods in Java 21. Explain in two sentences why a List implementation compiled against Java 8 still loads and works on Java 21.

Answer

Every new method is a default method on SequencedCollection/List, implemented in terms of methods the old class already has (iterator, listIterator, size, add), so the JVM resolves calls to the interface default when the class provides none. Only a class that already declared a same-named method with an incompatible return type would fail, and the JDK team searched Maven Central to make sure that set was tiny.

Misconceptions

  • "reversed() copies the collection." It is an O(1) live view; writes go through to the original.
  • "Every collection is sequenced now." Only ordered ones: lists, deques, linked and sorted sets and maps. HashSet/HashMap do not implement it.
  • "String templates are a Java 21 feature." They were a preview, withdrawn in Java 23, and do not compile on any current release.
  • "Preview features are safe if you pass --enable-preview." They change between releases, and code compiled with them refuses to run on a different feature version.
  • "Flexible constructors let you use this before super()." Only statements that do not reference this (and, since 25, assignments to this class's own fields) are allowed in the prologue.

Going deeper

  • JEP 431 (sequenced collections) and Stuart Marks's design note on the retrofit and the Maven Central compatibility scan.
  • ReverseOrderListView, ReverseOrderSortedSetView in java.util: the view classes.
  • JEP 456 (unnamed variables), JEP 513 (flexible constructor bodies), JEP 485 (gatherers), JEP 506 (scoped values), JEP 505 (structured concurrency), JEP 511 (module imports), JEP 512 (compact source files).
  • The JDK release notes "Removed Features and Options" section for each release you skip.
  • Nicolai Parlog, "Java 21 → 25: What Changed", for a one-page map.
Progress is saved on this device and to your account when signed in.