reversed();
+ public default java.util.SequencedCollection reversed();
+}
diff --git a/sequenced/output/01-across-collections-demo.txt b/sequenced/output/01-across-collections-demo.txt
new file mode 100644
index 0000000..2687bc7
--- /dev/null
+++ b/sequenced/output/01-across-collections-demo.txt
@@ -0,0 +1,28 @@
+=== List (ArrayList implements SequencedCollection) ===
+getFirst() = alpha
+getLast() = gamma
+after addLast("delta"): [alpha, beta, gamma, delta]
+reversed() = [delta, gamma, beta, alpha]
+
+=== Deque (ArrayDeque implements SequencedCollection) ===
+getFirst() = 1
+getLast() = 3
+reversed() = [3, 2, 1]
+
+=== LinkedHashSet implements SequencedSet (insertion order preserved) ===
+getFirst() = red
+getLast() = blue
+reversed() = [blue, green, red]
+
+=== LinkedHashMap implements SequencedMap (insertion order) ===
+firstEntry() = jan=1
+lastEntry() = mar=3
+after putFirst("dec", 12): {dec=12, jan=1, feb=2, mar=3}
+sequencedKeySet() = [dec, jan, feb, mar]
+reversed() = {mar=3, feb=2, jan=1, dec=12}
+
+=== TreeMap implements SequencedMap too (order = comparator order, not insertion) ===
+tree contents (natural order) = {1=one, 2=two, 3=three}
+firstEntry() = 1=one
+lastEntry() = 3=three
+reversed() = {3=three, 2=two, 1=one}
diff --git a/sequenced/output/02-find-last-demo.txt b/sequenced/output/02-find-last-demo.txt
new file mode 100644
index 0000000..76dd2c7
--- /dev/null
+++ b/sequenced/output/02-find-last-demo.txt
@@ -0,0 +1,22 @@
+A LinkedHashSet has a real iteration order, but pre-Java-21 Set has no getLast():
+[home, search, product/42, cart, checkout]
+
+Old #1 - stream().reduce((a,b) -> b): checkout
+Old #2 - stream().skip(size - 1).findFirst(): checkout
+Old #3 - new ArrayList<>(set).get(size-1): checkout
+New - set.getLast(): checkout
+
+All four agree: true
+
+Why the old ways were worse than they looked:
+ - reduce((a,b)->b) walks the ENTIRE set, every time: O(n).
+ - skip(size-1).findFirst() also walks the entire stream pipeline up to size-1: O(n).
+ - new ArrayList<>(set) copies every element just to throw the copy away: O(n) time AND space.
+ - set.getLast() walks the SequencedCollection's default path to reversed().iterator().next();
+ for LinkedHashSet specifically, that's backed by LinkedHashMap's existing doubly-linked
+ insertion-order list, so it resolves in O(1) in the JDK's actual implementation - though
+ that O(1) is an implementation detail of LinkedHashMap, not a guarantee the interface makes.
+
+On a List this was never actually hard - list.get(list.size()-1) already worked.
+list.getLast() = c (same answer, no size() arithmetic, and it now
+reads identically whether the underlying type is a List, a Deque, or a LinkedHashSet).
diff --git a/sequenced/output/03-reversed-view-is-live-demo.txt b/sequenced/output/03-reversed-view-is-live-demo.txt
new file mode 100644
index 0000000..177d4f7
--- /dev/null
+++ b/sequenced/output/03-reversed-view-is-live-demo.txt
@@ -0,0 +1,15 @@
+original = [10, 20, 30]
+reversedView = [30, 20, 10]
+
+Mutating the ORIGINAL list (original.add(40)):
+original = [10, 20, 30, 40]
+reversedView = [40, 30, 20, 10] <- updated with no code touching reversedView directly
+
+Mutating the REVERSED VIEW (reversedView.addFirst(0)):
+reversedView = [0, 40, 30, 20, 10]
+original = [10, 20, 30, 40, 0] <- the 0 landed at the END of the original list
+
+This is a VIEW, confirmed structurally: reversed().reversed() is back to the
+same logical order as the original, still backed by the same list:
+letters = [x, y, zed]
+letters.reversed().reversed() = [x, y, zed] (back to original order, same backing list)
diff --git a/sequenced/output/04-correctness-test.txt b/sequenced/output/04-correctness-test.txt
new file mode 100644
index 0000000..098889b
--- /dev/null
+++ b/sequenced/output/04-correctness-test.txt
@@ -0,0 +1,4 @@
+-------------------------------------------------------------------------------
+Test set: com.ankurm.sequenced.SequencedCollectionsTest
+-------------------------------------------------------------------------------
+Tests run: 6, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.158 s -- in com.ankurm.sequenced.SequencedCollectionsTest
diff --git a/sequenced/pom.xml b/sequenced/pom.xml
new file mode 100644
index 0000000..28d5645
--- /dev/null
+++ b/sequenced/pom.xml
@@ -0,0 +1,43 @@
+
+
+ 4.0.0
+
+
+ com.ankurm
+ java-core-examples
+ 1.0
+
+
+ sequenced
+ sequenced
+ JEP 431 Sequenced Collections: getFirst/getLast/reversed() across List, Deque, LinkedHashSet, LinkedHashMap and TreeMap, and why they replace the "find the last element" stream tricks.
+
+
+
+ org.junit.jupiter
+ junit-jupiter
+ 5.11.0
+ test
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 3.13.0
+
+ 25
+
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+ 3.2.5
+
+
+
+
diff --git a/sequenced/scripts/run-all.sh b/sequenced/scripts/run-all.sh
new file mode 100755
index 0000000..4aa8674
--- /dev/null
+++ b/sequenced/scripts/run-all.sh
@@ -0,0 +1,21 @@
+#!/usr/bin/env bash
+set -euo pipefail
+cd "$(dirname "$0")/.."
+
+javap java.util.SequencedCollection > /tmp/jp1.txt 2>&1
+javap java.util.SequencedMap >> /tmp/jp1.txt 2>&1
+javap java.util.SequencedSet >> /tmp/jp1.txt 2>&1
+grep -vE 'JAVA_TOOL_OPTIONS|Djavax\.net\.ssl|^WARNING' /tmp/jp1.txt > output/00-javap-sequenced-interfaces.txt
+
+mvn -q -B compile
+java -cp target/classes com.ankurm.sequenced.AcrossCollectionsDemo 2>&1 \
+ | grep -vE 'JAVA_TOOL_OPTIONS|Djavax\.net\.ssl|^WARNING' > output/01-across-collections-demo.txt
+java -cp target/classes com.ankurm.sequenced.FindLastDemo 2>&1 \
+ | grep -vE 'JAVA_TOOL_OPTIONS|Djavax\.net\.ssl|^WARNING' > output/02-find-last-demo.txt
+java -cp target/classes com.ankurm.sequenced.ReversedViewIsLiveDemo 2>&1 \
+ | grep -vE 'JAVA_TOOL_OPTIONS|Djavax\.net\.ssl|^WARNING' > output/03-reversed-view-is-live-demo.txt
+
+mvn -q -B test
+cp target/surefire-reports/com.ankurm.sequenced.SequencedCollectionsTest.txt output/04-correctness-test.txt
+
+echo "Regenerated output/00-04."
diff --git a/sequenced/scripts/run.sh b/sequenced/scripts/run.sh
new file mode 100755
index 0000000..99be3d2
--- /dev/null
+++ b/sequenced/scripts/run.sh
@@ -0,0 +1,6 @@
+#!/usr/bin/env bash
+# Run one of the demo classes ad hoc, e.g.: ./scripts/run.sh FindLastDemo
+set -euo pipefail
+cd "$(dirname "$0")/.."
+mvn -q -B compile
+java -cp target/classes "com.ankurm.sequenced.$1"
diff --git a/sequenced/src/main/java/com/ankurm/sequenced/AcrossCollectionsDemo.java b/sequenced/src/main/java/com/ankurm/sequenced/AcrossCollectionsDemo.java
new file mode 100644
index 0000000..d6154d9
--- /dev/null
+++ b/sequenced/src/main/java/com/ankurm/sequenced/AcrossCollectionsDemo.java
@@ -0,0 +1,76 @@
+package com.ankurm.sequenced;
+
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.SequencedCollection;
+import java.util.SequencedMap;
+import java.util.SequencedSet;
+import java.util.TreeMap;
+
+/**
+ * JEP 431 (Java 21) added three interfaces to {@code java.util}: {@link SequencedCollection},
+ * {@link SequencedSet} and {@link SequencedMap}. This class runs the same five operations -
+ * {@code getFirst}, {@code getLast}, {@code addFirst}, {@code addLast}, {@code reversed()} -
+ * against every collection family that picked them up, so the one shared contract is visible
+ * across types that previously had no common "has an encounter order" interface at all.
+ *
+ * Confirmed against the compiled JDK with {@code javap java.util.SequencedCollection} /
+ * {@code SequencedMap} / {@code SequencedSet} - see {@code output/00-javap-sequenced-interfaces.txt}.
+ */
+public final class AcrossCollectionsDemo {
+
+ private AcrossCollectionsDemo() {}
+
+ public static void main(String[] args) {
+ System.out.println("=== List (ArrayList implements SequencedCollection) ===");
+ List list = new ArrayList<>(List.of("alpha", "beta", "gamma"));
+ SequencedCollection listSeq = list; // List extends SequencedCollection
+ System.out.println("getFirst() = " + listSeq.getFirst());
+ System.out.println("getLast() = " + listSeq.getLast());
+ listSeq.addLast("delta");
+ System.out.println("after addLast(\"delta\"): " + list);
+ System.out.println("reversed() = " + listSeq.reversed());
+
+ System.out.println();
+ System.out.println("=== Deque (ArrayDeque implements SequencedCollection) ===");
+ ArrayDeque deque = new ArrayDeque<>(List.of(1, 2, 3));
+ System.out.println("getFirst() = " + deque.getFirst());
+ System.out.println("getLast() = " + deque.getLast());
+ System.out.println("reversed() = " + deque.reversed());
+
+ System.out.println();
+ System.out.println("=== LinkedHashSet implements SequencedSet (insertion order preserved) ===");
+ LinkedHashSet set = new LinkedHashSet<>(List.of("red", "green", "blue"));
+ SequencedSet setSeq = set;
+ System.out.println("getFirst() = " + setSeq.getFirst());
+ System.out.println("getLast() = " + setSeq.getLast());
+ System.out.println("reversed() = " + setSeq.reversed());
+
+ System.out.println();
+ System.out.println("=== LinkedHashMap implements SequencedMap (insertion order) ===");
+ LinkedHashMap lhm = new LinkedHashMap<>();
+ lhm.put("jan", 1);
+ lhm.put("feb", 2);
+ lhm.put("mar", 3);
+ SequencedMap lhmSeq = lhm;
+ System.out.println("firstEntry() = " + lhmSeq.firstEntry());
+ System.out.println("lastEntry() = " + lhmSeq.lastEntry());
+ lhmSeq.putFirst("dec", 12); // moves/inserts "dec" at the front
+ System.out.println("after putFirst(\"dec\", 12): " + lhm);
+ System.out.println("sequencedKeySet() = " + lhmSeq.sequencedKeySet());
+ System.out.println("reversed() = " + lhmSeq.reversed());
+
+ System.out.println();
+ System.out.println("=== TreeMap implements SequencedMap too (order = comparator order, not insertion) ===");
+ TreeMap tree = new TreeMap<>(Map.of(3, "three", 1, "one", 2, "two"));
+ SequencedMap treeSeq = tree;
+ System.out.println("tree contents (natural order) = " + tree);
+ System.out.println("firstEntry() = " + treeSeq.firstEntry());
+ System.out.println("lastEntry() = " + treeSeq.lastEntry());
+ System.out.println("reversed() = " + treeSeq.reversed());
+ }
+}
diff --git a/sequenced/src/main/java/com/ankurm/sequenced/FindLastDemo.java b/sequenced/src/main/java/com/ankurm/sequenced/FindLastDemo.java
new file mode 100644
index 0000000..3010c44
--- /dev/null
+++ b/sequenced/src/main/java/com/ankurm/sequenced/FindLastDemo.java
@@ -0,0 +1,70 @@
+package com.ankurm.sequenced;
+
+import java.util.ArrayList;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Optional;
+import java.util.Set;
+
+/**
+ * The specific pain point JEP 431 fixes: before Java 21, {@link Set} had no concept of "first" or
+ * "last" at all - not because sets are unordered (a {@link LinkedHashSet} very much has a defined
+ * iteration order) but because {@code Set} the interface never exposed one. The workarounds people
+ * reached for were stream tricks, and this class runs both the old workaround and the new one-liner
+ * side by side on the same {@code LinkedHashSet}.
+ */
+public final class FindLastDemo {
+
+ private FindLastDemo() {}
+
+ public static void main(String[] args) {
+ LinkedHashSet visited = new LinkedHashSet<>();
+ visited.add("home");
+ visited.add("search");
+ visited.add("product/42");
+ visited.add("cart");
+ visited.add("checkout");
+
+ System.out.println("A LinkedHashSet has a real iteration order, but pre-Java-21 Set has no getLast():");
+ System.out.println(visited);
+ System.out.println();
+
+ // --- Old workaround 1: reduce to the last stream element ---
+ Optional viaReduce = visited.stream().reduce((first, second) -> second);
+ System.out.println("Old #1 - stream().reduce((a,b) -> b): " + viaReduce.orElseThrow());
+
+ // --- Old workaround 2: skip to the end ---
+ Optional viaSkip = visited.stream().skip(visited.size() - 1L).findFirst();
+ System.out.println("Old #2 - stream().skip(size - 1).findFirst(): " + viaSkip.orElseThrow());
+
+ // --- Old workaround 3: copy into an indexable list just to index it ---
+ String viaCopy = new ArrayList<>(visited).get(visited.size() - 1);
+ System.out.println("Old #3 - new ArrayList<>(set).get(size-1): " + viaCopy);
+
+ // --- New: Java 21+, Set extends SequencedCollection via SequencedSet ---
+ String viaGetLast = visited.getLast();
+ System.out.println("New - set.getLast(): " + viaGetLast);
+
+ System.out.println();
+ boolean allFourAgree = viaReduce.get().equals(viaSkip.get())
+ && viaSkip.get().equals(viaCopy)
+ && viaCopy.equals(viaGetLast);
+ System.out.println("All four agree: " + allFourAgree);
+
+ System.out.println();
+ System.out.println("Why the old ways were worse than they looked:");
+ System.out.println(" - reduce((a,b)->b) walks the ENTIRE set, every time: O(n).");
+ System.out.println(" - skip(size-1).findFirst() also walks the entire stream pipeline up to size-1: O(n).");
+ System.out.println(" - new ArrayList<>(set) copies every element just to throw the copy away: O(n) time AND space.");
+ System.out.println(" - set.getLast() walks the SequencedCollection's default path to reversed().iterator().next();");
+ System.out.println(" for LinkedHashSet specifically, that's backed by LinkedHashMap's existing doubly-linked");
+ System.out.println(" insertion-order list, so it resolves in O(1) in the JDK's actual implementation - though");
+ System.out.println(" that O(1) is an implementation detail of LinkedHashMap, not a guarantee the interface makes.");
+
+ List asList = List.of("a", "b", "c");
+ System.out.println();
+ System.out.println("On a List this was never actually hard - list.get(list.size()-1) already worked.");
+ System.out.println("list.getLast() = " + asList.getLast() + " (same answer, no size() arithmetic, and it now");
+ System.out.println("reads identically whether the underlying type is a List, a Deque, or a LinkedHashSet).");
+ }
+}
diff --git a/sequenced/src/main/java/com/ankurm/sequenced/ReversedViewIsLiveDemo.java b/sequenced/src/main/java/com/ankurm/sequenced/ReversedViewIsLiveDemo.java
new file mode 100644
index 0000000..3a5972e
--- /dev/null
+++ b/sequenced/src/main/java/com/ankurm/sequenced/ReversedViewIsLiveDemo.java
@@ -0,0 +1,46 @@
+package com.ankurm.sequenced;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.SequencedCollection;
+
+/**
+ * {@code reversed()} returns a view, not a copy - mutations through either the
+ * original collection or the reversed view are visible through the other, immediately. This is
+ * the single most common assumption bug this topic produces: code that does
+ * {@code var snapshot = list.reversed();} and expects {@code snapshot} to be independent of later
+ * changes to {@code list}.
+ */
+public final class ReversedViewIsLiveDemo {
+
+ private ReversedViewIsLiveDemo() {}
+
+ public static void main(String[] args) {
+ List original = new ArrayList<>(List.of(10, 20, 30));
+ SequencedCollection reversedView = original.reversed();
+
+ System.out.println("original = " + original);
+ System.out.println("reversedView = " + reversedView);
+
+ System.out.println();
+ System.out.println("Mutating the ORIGINAL list (original.add(40)):");
+ original.add(40);
+ System.out.println("original = " + original);
+ System.out.println("reversedView = " + reversedView + " <- updated with no code touching reversedView directly");
+
+ System.out.println();
+ System.out.println("Mutating the REVERSED VIEW (reversedView.addFirst(0)):");
+ reversedView.addFirst(0); // "first" of the reversed view = "last" of the original
+ System.out.println("reversedView = " + reversedView);
+ System.out.println("original = " + original + " <- the 0 landed at the END of the original list");
+
+ System.out.println();
+ System.out.println("This is a VIEW, confirmed structurally: reversed().reversed() is back to the");
+ System.out.println("same logical order as the original, still backed by the same list:");
+ List letters = new ArrayList<>(List.of("x", "y", "zed"));
+ var once = letters.reversed();
+ var twice = once.reversed();
+ System.out.println("letters = " + letters);
+ System.out.println("letters.reversed().reversed() = " + twice + " (back to original order, same backing list)");
+ }
+}
diff --git a/sequenced/src/test/java/com/ankurm/sequenced/SequencedCollectionsTest.java b/sequenced/src/test/java/com/ankurm/sequenced/SequencedCollectionsTest.java
new file mode 100644
index 0000000..bf5b254
--- /dev/null
+++ b/sequenced/src/test/java/com/ankurm/sequenced/SequencedCollectionsTest.java
@@ -0,0 +1,70 @@
+package com.ankurm.sequenced;
+
+import org.junit.jupiter.api.Test;
+
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.SequencedCollection;
+import java.util.SequencedMap;
+import java.util.TreeMap;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+class SequencedCollectionsTest {
+
+ @Test
+ void listIsASequencedCollection() {
+ List list = new ArrayList<>(List.of("a", "b", "c"));
+ assertEquals("a", list.getFirst());
+ assertEquals("c", list.getLast());
+ assertEquals(List.of("c", "b", "a"), list.reversed());
+ }
+
+ @Test
+ void linkedHashSetGetsFirstAndLastThatPlainSetNeverHad() {
+ LinkedHashSet set = new LinkedHashSet<>(List.of("one", "two", "three"));
+ assertEquals("one", set.getFirst());
+ assertEquals("three", set.getLast());
+ }
+
+ @Test
+ void linkedHashMapPutFirstReordersInsteadOfJustInserting() {
+ LinkedHashMap map = new LinkedHashMap<>();
+ map.put("b", 2);
+ map.put("c", 3);
+ ((SequencedMap) map).putFirst("a", 1);
+ // putFirst moves "a" to the front of the iteration order, not just adds it
+ assertEquals(List.of("a", "b", "c"), new ArrayList<>(map.keySet()));
+ }
+
+ @Test
+ void treeMapOrderIsComparatorOrderNotInsertionOrder() {
+ TreeMap tree = new TreeMap<>(Map.of(3, "three", 1, "one", 2, "two"));
+ SequencedMap seq = tree;
+ assertEquals(1, seq.firstEntry().getKey());
+ assertEquals(3, seq.lastEntry().getKey());
+ }
+
+ @Test
+ void reversedIsALiveViewNotACopy() {
+ List list = new ArrayList<>(List.of(1, 2, 3));
+ SequencedCollection view = list.reversed();
+ list.add(4);
+ assertEquals(List.of(4, 3, 2, 1), view, "the view must see the mutation on the original");
+ view.addFirst(0); // "first of reversed" = "last of original"
+ assertEquals(List.of(1, 2, 3, 4, 0), list, "the original must see the mutation through the view");
+ }
+
+ @Test
+ void arrayDequeImplementsSequencedCollectionToo() {
+ ArrayDeque deque = new ArrayDeque<>(List.of("x", "y", "z"));
+ assertEquals("x", deque.getFirst());
+ assertEquals("z", deque.getLast());
+ // reversed() flips which end is "first" - the original's last is the reversed view's first.
+ assertEquals(deque.getLast(), deque.reversed().getFirst());
+ }
+}