diff --git a/README.md b/README.md index 98eb66e..e8de277 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,7 @@ article; each module's own README has that article's version table, quickstart, | [`collections`](collections/) | How to Sort a HashMap by Value (and Key) in Java | | [`nio2`](nio2/) | Java NIO.2 File API: Files, Path, WatchService, and Streaming Large Files Without OOM | | [`list-benchmarks`](list-benchmarks/) | ArrayList vs LinkedList in 2026: JMH Benchmarks and Why LinkedList Rarely Wins | +| [`sequenced`](sequenced/) | Sequenced Collections in Java 21+: getFirst, getLast and reversed() | ## License diff --git a/pom.xml b/pom.xml index 03a8228..0919ad3 100644 --- a/pom.xml +++ b/pom.xml @@ -29,6 +29,7 @@ collections nio2 list-benchmarks + sequenced diff --git a/sequenced/README.md b/sequenced/README.md new file mode 100644 index 0000000..095b3c6 --- /dev/null +++ b/sequenced/README.md @@ -0,0 +1,46 @@ +# sequenced + +Companion code for the ankurm.com post *"Sequenced Collections in Java 21+: getFirst, getLast and +reversed()."* Module in `java-core-examples`, the Java-core series. + +## Versions this was built and tested against + +| Component | Version | Notes | +|---|---|---| +| JDK | 25.0.4.1+1 (Temurin, LTS) | JEP 431 shipped in JDK 21 (GA September 2023) and is unchanged since. | +| JUnit Jupiter | 5.11.0 | | +| Maven | 3.9.11 | | + +## Quickstart + +```bash +export JAVA_HOME=/path/to/jdk-21-or-newer +mvn compile +java -cp target/classes com.ankurm.sequenced.AcrossCollectionsDemo +java -cp target/classes com.ankurm.sequenced.FindLastDemo +java -cp target/classes com.ankurm.sequenced.ReversedViewIsLiveDemo +``` + +`scripts/run-all.sh` regenerates every file in `output/`. `scripts/run.sh ` runs one +demo ad hoc. + +## What's in here + +| File | What it shows | +|---|---| +| `src/main/java/.../AcrossCollectionsDemo.java` | `getFirst`/`getLast`/`addFirst`/`addLast`/`reversed()` run against `ArrayList`, `ArrayDeque`, `LinkedHashSet`, `LinkedHashMap` and `TreeMap` - the one shared contract across five previously-unrelated types. | +| `src/main/java/.../FindLastDemo.java` | The three pre-Java-21 ways people found "the last element" of a `Set` (two stream tricks and a throwaway copy) next to the one-line `getLast()` replacement, with a note on why the old ways cost more than they looked like they did. | +| `src/main/java/.../ReversedViewIsLiveDemo.java` | `reversed()` is a live view, not a copy - mutating either side is visible through the other, immediately. | +| `src/test/java/.../SequencedCollectionsTest.java` | Pins all of the above as assertions, 6/6 passing. | +| `output/00-javap-sequenced-interfaces.txt` | `javap` output for `SequencedCollection`, `SequencedMap`, `SequencedSet` straight from the compiled JDK - the primary-source check this repo's discipline requires before describing any interface's method list. | +| `output/01-04` | Captured runs of the three demos and the test suite. | + +## Notes worth knowing before reading the post + +- **`TreeMap`/`TreeSet` already had most of this.** `firstEntry()`/`lastEntry()`/`pollFirstEntry()`/`pollLastEntry()` existed on `NavigableMap` since Java 6; what JEP 431 adds for sorted collections is mostly `reversed()` as a named counterpart to `descendingMap()`/`descendingSet()`, plus the shared interface. The real new capability lands on `LinkedHashSet` and `LinkedHashMap`, which had an iteration order but no vocabulary for "first" or "last" before this. +- **`reversed()` is a view.** See `output/03` - mutating the original after taking `reversed()` is visible through the view and vice versa. This is the most common assumption bug the feature produces. +- **`LinkedHashSet.getLast()` is O(1) in practice**, not by interface contract - it resolves through `SequencedCollection`'s default method path to the reversed view's first element, and `LinkedHashSet`/`LinkedHashMap` already maintain a doubly-linked insertion-order list internally, so there's no O(n) walk. That is an implementation detail of the JDK's `LinkedHashMap`, not a guarantee `SequencedCollection` itself makes for arbitrary implementors. + +## License + +MIT - see the [repo-wide LICENSE](../LICENSE). diff --git a/sequenced/output/00-javap-sequenced-interfaces.txt b/sequenced/output/00-javap-sequenced-interfaces.txt new file mode 100644 index 0000000..67b0581 --- /dev/null +++ b/sequenced/output/00-javap-sequenced-interfaces.txt @@ -0,0 +1,28 @@ +Compiled from "SequencedCollection.java" +public interface java.util.SequencedCollection extends java.util.Collection { + public abstract java.util.SequencedCollection reversed(); + public default void addFirst(E); + public default void addLast(E); + public default E getFirst(); + public default E getLast(); + public default E removeFirst(); + public default E removeLast(); +} +Compiled from "SequencedMap.java" +public interface java.util.SequencedMap extends java.util.Map { + public abstract java.util.SequencedMap reversed(); + public default java.util.Map$Entry firstEntry(); + public default java.util.Map$Entry lastEntry(); + public default java.util.Map$Entry pollFirstEntry(); + public default java.util.Map$Entry pollLastEntry(); + public default V putFirst(K, V); + public default V putLast(K, V); + public default java.util.SequencedSet sequencedKeySet(); + public default java.util.SequencedCollection sequencedValues(); + public default java.util.SequencedSet> sequencedEntrySet(); +} +Compiled from "SequencedSet.java" +public interface java.util.SequencedSet extends java.util.SequencedCollection, java.util.Set { + public abstract java.util.SequencedSet 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()); + } +}