sequenced: JEP 431 Sequenced Collections across List/Deque/LinkedHashSet/LinkedHashMap/TreeMap, and the getLast() replacement for the Set find-last stream tricks
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
<module>collections</module>
|
||||
<module>nio2</module>
|
||||
<module>list-benchmarks</module>
|
||||
<module>sequenced</module>
|
||||
</modules>
|
||||
|
||||
<properties>
|
||||
|
||||
@@ -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 <ClassName>` 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).
|
||||
@@ -0,0 +1,28 @@
|
||||
Compiled from "SequencedCollection.java"
|
||||
public interface java.util.SequencedCollection<E> extends java.util.Collection<E> {
|
||||
public abstract java.util.SequencedCollection<E> 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<K, V> extends java.util.Map<K, V> {
|
||||
public abstract java.util.SequencedMap<K, V> reversed();
|
||||
public default java.util.Map$Entry<K, V> firstEntry();
|
||||
public default java.util.Map$Entry<K, V> lastEntry();
|
||||
public default java.util.Map$Entry<K, V> pollFirstEntry();
|
||||
public default java.util.Map$Entry<K, V> pollLastEntry();
|
||||
public default V putFirst(K, V);
|
||||
public default V putLast(K, V);
|
||||
public default java.util.SequencedSet<K> sequencedKeySet();
|
||||
public default java.util.SequencedCollection<V> sequencedValues();
|
||||
public default java.util.SequencedSet<java.util.Map$Entry<K, V>> sequencedEntrySet();
|
||||
}
|
||||
Compiled from "SequencedSet.java"
|
||||
public interface java.util.SequencedSet<E> extends java.util.SequencedCollection<E>, java.util.Set<E> {
|
||||
public abstract java.util.SequencedSet<E> reversed();
|
||||
public default java.util.SequencedCollection reversed();
|
||||
}
|
||||
@@ -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}
|
||||
@@ -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).
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -0,0 +1,43 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>com.ankurm</groupId>
|
||||
<artifactId>java-core-examples</artifactId>
|
||||
<version>1.0</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>sequenced</artifactId>
|
||||
<name>sequenced</name>
|
||||
<description>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.</description>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter</artifactId>
|
||||
<version>5.11.0</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>3.13.0</version>
|
||||
<configuration>
|
||||
<release>25</release>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
<version>3.2.5</version>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
Executable
+21
@@ -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."
|
||||
Executable
+6
@@ -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"
|
||||
@@ -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.
|
||||
*
|
||||
* <p>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<String> list = new ArrayList<>(List.of("alpha", "beta", "gamma"));
|
||||
SequencedCollection<String> listSeq = list; // List<E> extends SequencedCollection<E>
|
||||
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<Integer> 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<String> set = new LinkedHashSet<>(List.of("red", "green", "blue"));
|
||||
SequencedSet<String> 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<String, Integer> lhm = new LinkedHashMap<>();
|
||||
lhm.put("jan", 1);
|
||||
lhm.put("feb", 2);
|
||||
lhm.put("mar", 3);
|
||||
SequencedMap<String, Integer> 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<Integer, String> tree = new TreeMap<>(Map.of(3, "three", 1, "one", 2, "two"));
|
||||
SequencedMap<Integer, String> 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());
|
||||
}
|
||||
}
|
||||
@@ -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<String> 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<String> 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<String> 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<String> 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).");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package com.ankurm.sequenced;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.SequencedCollection;
|
||||
|
||||
/**
|
||||
* {@code reversed()} returns a <strong>view</strong>, 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<Integer> original = new ArrayList<>(List.of(10, 20, 30));
|
||||
SequencedCollection<Integer> 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<String> 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)");
|
||||
}
|
||||
}
|
||||
@@ -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<String> 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<String> set = new LinkedHashSet<>(List.of("one", "two", "three"));
|
||||
assertEquals("one", set.getFirst());
|
||||
assertEquals("three", set.getLast());
|
||||
}
|
||||
|
||||
@Test
|
||||
void linkedHashMapPutFirstReordersInsteadOfJustInserting() {
|
||||
LinkedHashMap<String, Integer> map = new LinkedHashMap<>();
|
||||
map.put("b", 2);
|
||||
map.put("c", 3);
|
||||
((SequencedMap<String, Integer>) 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<Integer, String> tree = new TreeMap<>(Map.of(3, "three", 1, "one", 2, "two"));
|
||||
SequencedMap<Integer, String> seq = tree;
|
||||
assertEquals(1, seq.firstEntry().getKey());
|
||||
assertEquals(3, seq.lastEntry().getKey());
|
||||
}
|
||||
|
||||
@Test
|
||||
void reversedIsALiveViewNotACopy() {
|
||||
List<Integer> list = new ArrayList<>(List.of(1, 2, 3));
|
||||
SequencedCollection<Integer> 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<String> 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());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user