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
// ── 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 — 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 contractThe reversed() View — A Live, Backed Reversal, Not a Copy
// ── 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