From 381f11930830e8184bcc372ea3985b4de826ee86 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 30 Sep 2026 19:10:01 +0000 Subject: [PATCH] collections: companion code for the How to Sort a HashMap by Value (and Key) post Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01KqJyCidz3ZgRyHABv2GVJh --- README.md | 1 + collections/README.md | 37 ++++++++ collections/output/01-sort-by-value.txt | 7 ++ collections/output/02-tie-bug.txt | 4 + collections/output/03-top-n.txt | 2 + collections/output/04-sequenced.txt | 9 ++ ...05-HashMapIsNotSequenced-compile-error.txt | 6 ++ .../06-SortMapDirectly-compile-error.txt | 13 +++ collections/output/07-jmh-raw.txt | 7 ++ collections/output/08-tests.txt | 4 + collections/pom.xml | 88 +++++++++++++++++++ collections/scripts/run-all.sh | 22 +++++ .../src/broken/HashMapIsNotSequenced.java | 11 +++ collections/src/broken/SortMapDirectly.java | 10 +++ .../java/com/ankurm/collections/Scores.java | 19 ++++ .../com/ankurm/collections/SequencedDemo.java | 45 ++++++++++ .../ankurm/collections/SortByValueDemo.java | 47 ++++++++++ .../ankurm/collections/SortingBenchmark.java | 50 +++++++++++ .../com/ankurm/collections/TieBugDemo.java | 30 +++++++ .../java/com/ankurm/collections/TopNDemo.java | 37 ++++++++ .../com/ankurm/collections/SortingTest.java | 59 +++++++++++++ pom.xml | 1 + 22 files changed, 509 insertions(+) create mode 100644 collections/README.md create mode 100644 collections/output/01-sort-by-value.txt create mode 100644 collections/output/02-tie-bug.txt create mode 100644 collections/output/03-top-n.txt create mode 100644 collections/output/04-sequenced.txt create mode 100644 collections/output/05-HashMapIsNotSequenced-compile-error.txt create mode 100644 collections/output/06-SortMapDirectly-compile-error.txt create mode 100644 collections/output/07-jmh-raw.txt create mode 100644 collections/output/08-tests.txt create mode 100644 collections/pom.xml create mode 100755 collections/scripts/run-all.sh create mode 100644 collections/src/broken/HashMapIsNotSequenced.java create mode 100644 collections/src/broken/SortMapDirectly.java create mode 100644 collections/src/main/java/com/ankurm/collections/Scores.java create mode 100644 collections/src/main/java/com/ankurm/collections/SequencedDemo.java create mode 100644 collections/src/main/java/com/ankurm/collections/SortByValueDemo.java create mode 100644 collections/src/main/java/com/ankurm/collections/SortingBenchmark.java create mode 100644 collections/src/main/java/com/ankurm/collections/TieBugDemo.java create mode 100644 collections/src/main/java/com/ankurm/collections/TopNDemo.java create mode 100644 collections/src/test/java/com/ankurm/collections/SortingTest.java diff --git a/README.md b/README.md index f7e1413..2edc4d6 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,7 @@ article; each module's own README has that article's version table, quickstart, | [`regex`](regex/) | Java Regex Tutorial: Pattern, Matcher, Groups, Lookarounds and ReDoS | | [`serialization`](serialization/) | Java Serialization in 2026: Why It's Dangerous and What Replaced It | | [`strings`](strings/) | Java Strings Deep Dive: Interning, StringBuilder, Text Blocks, and the Concatenation Benchmark | +| [`collections`](collections/) | How to Sort a HashMap by Value (and Key) in Java | ## License diff --git a/collections/README.md b/collections/README.md new file mode 100644 index 0000000..4e6639f --- /dev/null +++ b/collections/README.md @@ -0,0 +1,37 @@ +# collections + +Companion code for the ankurm.com post *"How to Sort a HashMap by Value (and Key) in Java."* +Module `collections` in `java-core-examples`. + +All explanation lives in the post; this module holds the runnable evidence and the captured output. + +## Versions + +| Component | Version | +|---|---| +| JDK | 25.0.4.1+1 (Temurin, LTS) | +| JMH | 1.37 | +| JUnit Jupiter | 5.11.0 | +| Hardware | 2 vCPU x86-64 VM, shared with other jobs (timings are indicative, not a leaderboard) | + +## Quickstart + +```bash +export JDK25_HOME=/path/to/jdk-25 +./scripts/run-all.sh # rebuilds and regenerates everything in output/ +``` + +## What is in here + +| File | Shows | Output | +|---|---|---| +| `SortByValueDemo` | stream into `LinkedHashMap`, descending, by key, list of entries, the `toMap()` default-HashMap trap, tie-break | `01` | +| `TieBugDemo` | a `TreeMap` with a value-only comparator silently drops equal values; the tie-break fix | `02` | +| `TopNDemo` | top-N by sort+limit and by a size-N `PriorityQueue` | `03` | +| `SequencedDemo` | JDK 21+ `reversed()`, `firstEntry()`, `putLast()`, live-view behaviour | `04` | +| `src/broken/*.java` | `HashMap` has no `firstEntry()`; `Collections.sort(map)` does not compile; javac's real message is captured | `05`-`06` | +| `SortingBenchmark` | JMH: sort-all vs heap top-10 at 1,000 and 100,000 entries; full sort into `LinkedHashMap` | `07` | +| `SortingTest` | 6 assertions behind the claims above | `08` | + +The JMH run is 2 forks, 3 warmup and 5 measurement iterations of 2 s per fork. The sandbox it ran on +was shared with other jobs, so re-running moves the numbers by tens of percent but not the ordering. diff --git a/collections/output/01-sort-by-value.txt b/collections/output/01-sort-by-value.txt new file mode 100644 index 0000000..0884a70 --- /dev/null +++ b/collections/output/01-sort-by-value.txt @@ -0,0 +1,7 @@ +HashMap order : {carol=85, dave=70, bob=70, alice=90, erin=95} +by value asc : {dave=70, bob=70, carol=85, alice=90, erin=95} +by value desc : {erin=95, alice=90, carol=85, dave=70, bob=70} +by key (Tree) : {alice=90, bob=70, carol=85, dave=70, erin=95} +toMap() no LHM: {carol=85, dave=70, bob=70, alice=90, erin=95} (class HashMap) +List : [erin=95, alice=90, carol=85, dave=70, bob=70] +value desc+key: {erin=95, alice=90, carol=85, bob=70, dave=70} diff --git a/collections/output/02-tie-bug.txt b/collections/output/02-tie-bug.txt new file mode 100644 index 0000000..69d33c3 --- /dev/null +++ b/collections/output/02-tie-bug.txt @@ -0,0 +1,4 @@ +input size : 5 +TreeMap(value): {dave=70, carol=85, alice=90, erin=95} size 4 +containsKey : bob true, dave true get(bob) = 70 +TreeMap fixed : {bob=70, dave=70, carol=85, alice=90, erin=95} size 5 diff --git a/collections/output/03-top-n.txt b/collections/output/03-top-n.txt new file mode 100644 index 0000000..c3336f5 --- /dev/null +++ b/collections/output/03-top-n.txt @@ -0,0 +1,2 @@ +sort + limit(3): [erin=95, alice=90, carol=85] +heap of size 3 : [erin=95, alice=90, carol=85] diff --git a/collections/output/04-sequenced.txt b/collections/output/04-sequenced.txt new file mode 100644 index 0000000..147f5f5 --- /dev/null +++ b/collections/output/04-sequenced.txt @@ -0,0 +1,9 @@ +ascending : {bob=70, carol=85, alice=90, erin=95} +reversed() : {erin=95, alice=90, carol=85, bob=70} +firstEntry() : bob=70 +lastEntry() : erin=95 +top2 (by pos) : [erin=95, alice=90] +after put via view (no longer sorted): {bob=70, carol=85, alice=90, erin=95, zed=99} +putLast/putFirst : {carol=85, alice=90, erin=95, zed=99, bob=70} +TreeMap first : alice=90 reversed: {zed=99, erin=95, carol=85, bob=70, alice=90} +pollFirstEntry: carol=85 -> {alice=90, erin=95, zed=99, bob=70} diff --git a/collections/output/05-HashMapIsNotSequenced-compile-error.txt b/collections/output/05-HashMapIsNotSequenced-compile-error.txt new file mode 100644 index 0000000..a6bdcef --- /dev/null +++ b/collections/output/05-HashMapIsNotSequenced-compile-error.txt @@ -0,0 +1,6 @@ +src/broken/HashMapIsNotSequenced.java:8: error: cannot find symbol + Map.Entry first = m.firstEntry(); + ^ + symbol: method firstEntry() + location: variable m of type HashMap +1 error diff --git a/collections/output/06-SortMapDirectly-compile-error.txt b/collections/output/06-SortMapDirectly-compile-error.txt new file mode 100644 index 0000000..00c3a14 --- /dev/null +++ b/collections/output/06-SortMapDirectly-compile-error.txt @@ -0,0 +1,13 @@ +src/broken/SortMapDirectly.java:8: error: no suitable method found for sort(Map) + java.util.Collections.sort(m); + ^ + method Collections.sort(List) is not applicable + (cannot infer type-variable(s) T#1 + (argument mismatch; Map cannot be converted to List)) + method Collections.sort(List,Comparator) is not applicable + (cannot infer type-variable(s) T#2 + (actual and formal argument lists differ in length)) + where T#1,T#2 are type-variables: + T#1 extends Comparable declared in method sort(List) + T#2 extends Object declared in method sort(List,Comparator) +1 error diff --git a/collections/output/07-jmh-raw.txt b/collections/output/07-jmh-raw.txt new file mode 100644 index 0000000..9a4c01b --- /dev/null +++ b/collections/output/07-jmh-raw.txt @@ -0,0 +1,7 @@ +Benchmark (size) Mode Cnt Score Error Units +SortingBenchmark.heapTopN 1000 avgt 10 10.344 ± 2.238 us/op +SortingBenchmark.heapTopN 100000 avgt 10 642.111 ± 92.327 us/op +SortingBenchmark.sortAllIntoLinkedHashMap 1000 avgt 10 121.877 ± 11.882 us/op +SortingBenchmark.sortAllIntoLinkedHashMap 100000 avgt 10 46582.889 ± 9003.854 us/op +SortingBenchmark.sortAllThenLimit 1000 avgt 10 55.609 ± 5.247 us/op +SortingBenchmark.sortAllThenLimit 100000 avgt 10 35128.268 ± 10045.080 us/op diff --git a/collections/output/08-tests.txt b/collections/output/08-tests.txt new file mode 100644 index 0000000..177fd43 --- /dev/null +++ b/collections/output/08-tests.txt @@ -0,0 +1,4 @@ +------------------------------------------------------------------------------- +Test set: com.ankurm.collections.SortingTest +------------------------------------------------------------------------------- +Tests run: 6, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.229 s -- in com.ankurm.collections.SortingTest diff --git a/collections/pom.xml b/collections/pom.xml new file mode 100644 index 0000000..1959eb2 --- /dev/null +++ b/collections/pom.xml @@ -0,0 +1,88 @@ + + + 4.0.0 + + + com.ankurm + java-core-examples + 1.0 + + + collections + collections + Sorting a Java Map by value and by key: stream into LinkedHashMap, TreeMap pitfalls, top-N, sequenced collections, and a JMH comparison. + + + 1.37 + + + + + org.openjdk.jmh + jmh-core + ${jmh.version} + + + org.openjdk.jmh + jmh-generator-annprocess + ${jmh.version} + + + org.junit.jupiter + junit-jupiter + 5.11.0 + test + + + + + benchmarks + + + org.apache.maven.plugins + maven-compiler-plugin + 3.13.0 + + 25 + + + + org.openjdk.jmh + jmh-generator-annprocess + ${jmh.version} + + + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.2.5 + + + org.apache.maven.plugins + maven-shade-plugin + 3.5.1 + + + package + shade + + + + org.openjdk.jmh.Main + + + + + + + + + diff --git a/collections/scripts/run-all.sh b/collections/scripts/run-all.sh new file mode 100755 index 0000000..7613b15 --- /dev/null +++ b/collections/scripts/run-all.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env bash +# Regenerates every file in ../output/. Requires JDK25_HOME. +set -euo pipefail +[[ -z "${JDK25_HOME:-}" ]] && { echo "JDK25_HOME must be set" >&2; exit 1; } +cd "$(dirname "$0")/.." +OUT=output; mkdir -p "$OUT" +export JAVA_HOME="$JDK25_HOME" +mvn -q -f ../pom.xml -pl collections -am package +J="$JDK25_HOME/bin/java" +run() { echo "==> $1"; "$J" -cp target/classes "com.ankurm.collections.$1" 2>&1 | grep -v "Picked up" > "$OUT/$2"; } +run SortByValueDemo 01-sort-by-value.txt +run TieBugDemo 02-tie-bug.txt +run TopNDemo 03-top-n.txt +run SequencedDemo 04-sequenced.txt +for b in HashMapIsNotSequenced:05 SortMapDirectly:06; do + n=${b%%:*}; k=${b##*:} + "$JDK25_HOME/bin/javac" -d /tmp/lc-coll src/broken/$n.java 2>&1 | grep -v "Picked up" > "$OUT/$k-$n-compile-error.txt" || true +done +echo "==> JMH" +"$J" -jar target/benchmarks.jar SortingBenchmark -rf text -rff "$OUT/07-jmh-raw.txt" 2>&1 | grep -v "Picked up" > /dev/null +cp target/surefire-reports/com.ankurm.collections.SortingTest.txt "$OUT/08-tests.txt" +echo Done diff --git a/collections/src/broken/HashMapIsNotSequenced.java b/collections/src/broken/HashMapIsNotSequenced.java new file mode 100644 index 0000000..bd24e30 --- /dev/null +++ b/collections/src/broken/HashMapIsNotSequenced.java @@ -0,0 +1,11 @@ +import java.util.HashMap; +import java.util.Map; + +public class HashMapIsNotSequenced { + public static void main(String[] args) { + HashMap m = new HashMap<>(); + m.put("a", 1); + Map.Entry first = m.firstEntry(); + System.out.println(first); + } +} diff --git a/collections/src/broken/SortMapDirectly.java b/collections/src/broken/SortMapDirectly.java new file mode 100644 index 0000000..cc033ec --- /dev/null +++ b/collections/src/broken/SortMapDirectly.java @@ -0,0 +1,10 @@ +import java.util.HashMap; +import java.util.Map; + +public class SortMapDirectly { + public static void main(String[] args) { + Map m = new HashMap<>(); + m.put("a", 1); + java.util.Collections.sort(m); + } +} diff --git a/collections/src/main/java/com/ankurm/collections/Scores.java b/collections/src/main/java/com/ankurm/collections/Scores.java new file mode 100644 index 0000000..95002b8 --- /dev/null +++ b/collections/src/main/java/com/ankurm/collections/Scores.java @@ -0,0 +1,19 @@ +package com.ankurm.collections; + +import java.util.HashMap; +import java.util.Map; + +/** Shared sample data: player scores, with a deliberate tie (bob and dave both have 70). */ +final class Scores { + private Scores() {} + + static Map sample() { + Map m = new HashMap<>(); + m.put("alice", 90); + m.put("bob", 70); + m.put("carol", 85); + m.put("dave", 70); + m.put("erin", 95); + return m; + } +} diff --git a/collections/src/main/java/com/ankurm/collections/SequencedDemo.java b/collections/src/main/java/com/ankurm/collections/SequencedDemo.java new file mode 100644 index 0000000..7ea2c31 --- /dev/null +++ b/collections/src/main/java/com/ankurm/collections/SequencedDemo.java @@ -0,0 +1,45 @@ +package com.ankurm.collections; + +import java.util.*; + +/** JDK 21+ sequenced collections (JEP 431): SequencedMap on LinkedHashMap and TreeMap. */ +public class SequencedDemo { + + public static void main(String[] args) { + LinkedHashMap asc = new LinkedHashMap<>(); + asc.put("bob", 70); + asc.put("carol", 85); + asc.put("alice", 90); + asc.put("erin", 95); + System.out.println("ascending : " + asc); + + // reversed() is a live VIEW, not a copy: no re-sort needed to read a sorted map backwards. + System.out.println("reversed() : " + asc.reversed()); + System.out.println("firstEntry() : " + asc.firstEntry()); + System.out.println("lastEntry() : " + asc.lastEntry()); + + // Top 2 of a map that is already sorted ascending: read from the end, no sort. + List> top2 = new ArrayList<>(); + for (var e : asc.reversed().sequencedEntrySet()) { + if (top2.size() == 2) break; + top2.add(e); + } + System.out.println("top2 (by pos) : " + top2); + + // Because it is a view, writes show through. + asc.reversed().put("zed", 99); + System.out.println("after put via view (no longer sorted): " + asc); + + // Re-ordering an existing LinkedHashMap: putLast moves an existing key to the end. + asc.putLast("bob", 70); + asc.putFirst("carol", 85); + System.out.println("putLast/putFirst : " + asc); + + // TreeMap is a SequencedMap too (via NavigableMap), ordered by key. + TreeMap tree = new TreeMap<>(asc); + System.out.println("TreeMap first : " + tree.firstEntry() + " reversed: " + tree.reversed()); + + // Views are live: a sequenced map is a snapshot of ORDER only while nobody mutates it. + System.out.println("pollFirstEntry: " + asc.pollFirstEntry() + " -> " + asc); + } +} diff --git a/collections/src/main/java/com/ankurm/collections/SortByValueDemo.java b/collections/src/main/java/com/ankurm/collections/SortByValueDemo.java new file mode 100644 index 0000000..7c76a91 --- /dev/null +++ b/collections/src/main/java/com/ankurm/collections/SortByValueDemo.java @@ -0,0 +1,47 @@ +package com.ankurm.collections; + +import java.util.*; +import java.util.stream.Collectors; + +public class SortByValueDemo { + + public static void main(String[] args) { + Map scores = Scores.sample(); + System.out.println("HashMap order : " + scores); + + // The shortest correct answer: stream the entries, sort them, collect into a LinkedHashMap. + Map byValue = scores.entrySet().stream() + .sorted(Map.Entry.comparingByValue()) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, + (a, b) -> a, LinkedHashMap::new)); + System.out.println("by value asc : " + byValue); + + Map byValueDesc = scores.entrySet().stream() + .sorted(Map.Entry.comparingByValue().reversed()) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, + (a, b) -> a, LinkedHashMap::new)); + System.out.println("by value desc : " + byValueDesc); + + // Sorting by key is easier: a TreeMap does it for you. + System.out.println("by key (Tree) : " + new TreeMap<>(scores)); + + // The trap: collecting into the default toMap() gives a HashMap, which forgets the order. + Map forgotten = scores.entrySet().stream() + .sorted(Map.Entry.comparingByValue()) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + System.out.println("toMap() no LHM: " + forgotten + " (class " + forgotten.getClass().getSimpleName() + ")"); + + // Often a List of entries is all you need (display, iteration): no map at all. + List> list = new ArrayList<>(scores.entrySet()); + list.sort(Map.Entry.comparingByValue(Comparator.reverseOrder())); + System.out.println("List : " + list); + + // Tie-break: value descending, then key ascending, so equal values come out in a stable, readable order. + Map tieBroken = scores.entrySet().stream() + .sorted(Map.Entry.comparingByValue().reversed() + .thenComparing(Map.Entry.comparingByKey())) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, + (a, b) -> a, LinkedHashMap::new)); + System.out.println("value desc+key: " + tieBroken); + } +} diff --git a/collections/src/main/java/com/ankurm/collections/SortingBenchmark.java b/collections/src/main/java/com/ankurm/collections/SortingBenchmark.java new file mode 100644 index 0000000..0a01d46 --- /dev/null +++ b/collections/src/main/java/com/ankurm/collections/SortingBenchmark.java @@ -0,0 +1,50 @@ +package com.ankurm.collections; + +import org.openjdk.jmh.annotations.*; +import java.util.*; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +/** + * Top-10 by value from a HashMap of n entries: sort everything then cut, versus a size-10 heap. + * Also the cost of sorting the full map into a LinkedHashMap. + */ +@BenchmarkMode(Mode.AverageTime) +@OutputTimeUnit(TimeUnit.MICROSECONDS) +@Warmup(iterations = 3, time = 2) +@Measurement(iterations = 5, time = 2) +@Fork(2) +@State(Scope.Benchmark) +public class SortingBenchmark { + + static final int N = 10; + + @Param({"1000", "100000"}) + public int size; + + Map map; + + @Setup + public void setup() { + Random r = new Random(42); + map = new HashMap<>(); + for (int i = 0; i < size; i++) map.put(i, r.nextInt()); + } + + @Benchmark + public List> sortAllThenLimit() { + return TopNDemo.topNBySorting(map, N); + } + + @Benchmark + public List> heapTopN() { + return TopNDemo.topN(map, N); + } + + @Benchmark + public Map sortAllIntoLinkedHashMap() { + return map.entrySet().stream() + .sorted(Map.Entry.comparingByValue()) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (a, b) -> a, LinkedHashMap::new)); + } +} diff --git a/collections/src/main/java/com/ankurm/collections/TieBugDemo.java b/collections/src/main/java/com/ankurm/collections/TieBugDemo.java new file mode 100644 index 0000000..b091cff --- /dev/null +++ b/collections/src/main/java/com/ankurm/collections/TieBugDemo.java @@ -0,0 +1,30 @@ +package com.ankurm.collections; + +import java.util.*; + +public class TieBugDemo { + + public static void main(String[] args) { + Map scores = Scores.sample(); + System.out.println("input size : " + scores.size()); + + // Tempting, and wrong: a TreeMap whose comparator looks only at the VALUE. + // TreeMap decides "same key" with compare()==0, so bob(70) and dave(70) collide. + Map byValue = new TreeMap<>(Comparator.comparing(scores::get)); + byValue.putAll(scores); + System.out.println("TreeMap(value): " + byValue + " size " + byValue.size()); + System.out.println("containsKey : bob " + byValue.containsKey("bob") + ", dave " + byValue.containsKey("dave") + " get(bob) = " + byValue.get("bob")); + + // Note: one of the two names is missing from the map, yet containsKey() says true for both, + // because lookups go through the same comparator. The entry is not "lost", it was never stored. + + // The fix inside a TreeMap: make the comparator total by falling back to the key. + Map fixed = new TreeMap<>( + Comparator.comparing((String k) -> scores.get(k)).thenComparing(Comparator.naturalOrder())); + fixed.putAll(scores); + System.out.println("TreeMap fixed : " + fixed + " size " + fixed.size()); + + // A TreeMap comparator that reads another map is fragile anyway: change a score and + // the tree is silently corrupt. Sorting a stream into a LinkedHashMap avoids that. + } +} diff --git a/collections/src/main/java/com/ankurm/collections/TopNDemo.java b/collections/src/main/java/com/ankurm/collections/TopNDemo.java new file mode 100644 index 0000000..0a25d3d --- /dev/null +++ b/collections/src/main/java/com/ankurm/collections/TopNDemo.java @@ -0,0 +1,37 @@ +package com.ankurm.collections; + +import java.util.*; +import java.util.stream.Collectors; + +public class TopNDemo { + + /** Top n entries by value, highest first, using a min-heap of size n. */ + static > List> topN(Map map, int n) { + PriorityQueue> heap = new PriorityQueue<>(Map.Entry.comparingByValue()); + for (Map.Entry e : map.entrySet()) { + if (heap.size() < n) { + heap.add(e); + } else if (e.getValue().compareTo(heap.peek().getValue()) > 0) { + heap.poll(); + heap.add(e); + } + } + List> result = new ArrayList<>(heap); + result.sort(Map.Entry.comparingByValue().reversed()); + return result; + } + + /** Top n by sorting everything, then cutting. */ + static > List> topNBySorting(Map map, int n) { + return map.entrySet().stream() + .sorted(Map.Entry.comparingByValue().reversed()) + .limit(n) + .collect(Collectors.toList()); + } + + public static void main(String[] args) { + Map scores = Scores.sample(); + System.out.println("sort + limit(3): " + topNBySorting(scores, 3)); + System.out.println("heap of size 3 : " + topN(scores, 3)); + } +} diff --git a/collections/src/test/java/com/ankurm/collections/SortingTest.java b/collections/src/test/java/com/ankurm/collections/SortingTest.java new file mode 100644 index 0000000..1682ea5 --- /dev/null +++ b/collections/src/test/java/com/ankurm/collections/SortingTest.java @@ -0,0 +1,59 @@ +package com.ankurm.collections; + +import static org.junit.jupiter.api.Assertions.*; +import org.junit.jupiter.api.Test; +import java.util.*; +import java.util.stream.Collectors; + +class SortingTest { + + @Test void streamIntoLinkedHashMapKeepsValueOrder() { + Map m = Scores.sample().entrySet().stream() + .sorted(Map.Entry.comparingByValue()) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (a, b) -> a, LinkedHashMap::new)); + assertEquals(List.of(70, 70, 85, 90, 95), new ArrayList<>(m.values())); + assertEquals(5, m.size()); + } + + @Test void valueOnlyTreeMapComparatorDropsTies() { + Map scores = Scores.sample(); + Map t = new TreeMap<>(Comparator.comparing(scores::get)); + t.putAll(scores); + assertEquals(4, t.size()); // bob or dave is gone + assertEquals(1, t.keySet().stream().filter(k -> k.equals("bob") || k.equals("dave")).count()); + assertTrue(t.containsKey("bob") && t.containsKey("dave")); // lookups go through the comparator, so both "exist" + } + + @Test void tieBreakingComparatorKeepsEverything() { + Map scores = Scores.sample(); + Map t = new TreeMap<>( + Comparator.comparing((String k) -> scores.get(k)).thenComparing(Comparator.naturalOrder())); + t.putAll(scores); + assertEquals(5, t.size()); + assertEquals(List.of("bob", "dave", "carol", "alice", "erin"), new ArrayList<>(t.keySet())); + } + + @Test void heapAndSortAgreeOnTopN() { + Random r = new Random(1); + Map m = new HashMap<>(); + for (int i = 0; i < 5000; i++) m.put(i, r.nextInt(100_000)); + var a = TopNDemo.topNBySorting(m, 10).stream().map(Map.Entry::getValue).toList(); + var b = TopNDemo.topN(m, 10).stream().map(Map.Entry::getValue).toList(); + assertEquals(a, b); + } + + @Test void reversedIsALiveView() { + LinkedHashMap m = new LinkedHashMap<>(); + m.put("a", 1); m.put("b", 2); + m.reversed().put("c", 3); + assertEquals(List.of("a", "b", "c"), new ArrayList<>(m.keySet())); + assertEquals("c", m.lastEntry().getKey()); + assertEquals(List.of("c", "b", "a"), new ArrayList<>(m.reversed().keySet())); + } + + @Test void defaultToMapReturnsHashMap() { + Map m = Scores.sample().entrySet().stream() + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + assertEquals(HashMap.class, m.getClass()); + } +} diff --git a/pom.xml b/pom.xml index 0ebe821..f1ae221 100644 --- a/pom.xml +++ b/pom.xml @@ -26,6 +26,7 @@ regex serialization strings + collections