collections: companion code for the How to Sort a HashMap by Value (and Key) post
Co-Authored-By: Claude Sonnet 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01KqJyCidz3ZgRyHABv2GVJh
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
@@ -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<Entry> : [erin=95, alice=90, carol=85, dave=70, bob=70]
|
||||
value desc+key: {erin=95, alice=90, carol=85, bob=70, dave=70}
|
||||
@@ -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
|
||||
@@ -0,0 +1,2 @@
|
||||
sort + limit(3): [erin=95, alice=90, carol=85]
|
||||
heap of size 3 : [erin=95, alice=90, carol=85]
|
||||
@@ -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}
|
||||
@@ -0,0 +1,6 @@
|
||||
src/broken/HashMapIsNotSequenced.java:8: error: cannot find symbol
|
||||
Map.Entry<String, Integer> first = m.firstEntry();
|
||||
^
|
||||
symbol: method firstEntry()
|
||||
location: variable m of type HashMap<String,Integer>
|
||||
1 error
|
||||
@@ -0,0 +1,13 @@
|
||||
src/broken/SortMapDirectly.java:8: error: no suitable method found for sort(Map<String,Integer>)
|
||||
java.util.Collections.sort(m);
|
||||
^
|
||||
method Collections.<T#1>sort(List<T#1>) is not applicable
|
||||
(cannot infer type-variable(s) T#1
|
||||
(argument mismatch; Map<String,Integer> cannot be converted to List<T#1>))
|
||||
method Collections.<T#2>sort(List<T#2>,Comparator<? super T#2>) 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<? super T#1> declared in method <T#1>sort(List<T#1>)
|
||||
T#2 extends Object declared in method <T#2>sort(List<T#2>,Comparator<? super T#2>)
|
||||
1 error
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -0,0 +1,88 @@
|
||||
<?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>collections</artifactId>
|
||||
<name>collections</name>
|
||||
<description>Sorting a Java Map by value and by key: stream into LinkedHashMap, TreeMap pitfalls, top-N, sequenced collections, and a JMH comparison.</description>
|
||||
|
||||
<properties>
|
||||
<jmh.version>1.37</jmh.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.openjdk.jmh</groupId>
|
||||
<artifactId>jmh-core</artifactId>
|
||||
<version>${jmh.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openjdk.jmh</groupId>
|
||||
<artifactId>jmh-generator-annprocess</artifactId>
|
||||
<version>${jmh.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter</artifactId>
|
||||
<version>5.11.0</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<finalName>benchmarks</finalName>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>3.13.0</version>
|
||||
<configuration>
|
||||
<release>25</release>
|
||||
<!--
|
||||
Same JDK-25-stops-discovering-annotation-processors-implicitly trap documented in
|
||||
the jmm module: JMH's @Benchmark-method-to-*_jmh.java generator needs to be declared
|
||||
explicitly here or the build silently produces a jar with nothing runnable in it.
|
||||
-->
|
||||
<annotationProcessorPaths>
|
||||
<path>
|
||||
<groupId>org.openjdk.jmh</groupId>
|
||||
<artifactId>jmh-generator-annprocess</artifactId>
|
||||
<version>${jmh.version}</version>
|
||||
</path>
|
||||
</annotationProcessorPaths>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
<version>3.2.5</version>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-shade-plugin</artifactId>
|
||||
<version>3.5.1</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<phase>package</phase>
|
||||
<goals><goal>shade</goal></goals>
|
||||
<configuration>
|
||||
<transformers>
|
||||
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
|
||||
<mainClass>org.openjdk.jmh.Main</mainClass>
|
||||
</transformer>
|
||||
</transformers>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
Executable
+22
@@ -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
|
||||
@@ -0,0 +1,11 @@
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public class HashMapIsNotSequenced {
|
||||
public static void main(String[] args) {
|
||||
HashMap<String, Integer> m = new HashMap<>();
|
||||
m.put("a", 1);
|
||||
Map.Entry<String, Integer> first = m.firstEntry();
|
||||
System.out.println(first);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public class SortMapDirectly {
|
||||
public static void main(String[] args) {
|
||||
Map<String, Integer> m = new HashMap<>();
|
||||
m.put("a", 1);
|
||||
java.util.Collections.sort(m);
|
||||
}
|
||||
}
|
||||
@@ -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<String, Integer> sample() {
|
||||
Map<String, Integer> 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;
|
||||
}
|
||||
}
|
||||
@@ -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<String, Integer> 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<Map.Entry<String, Integer>> 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<String, Integer> 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);
|
||||
}
|
||||
}
|
||||
@@ -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<String, Integer> scores = Scores.sample();
|
||||
System.out.println("HashMap order : " + scores);
|
||||
|
||||
// The shortest correct answer: stream the entries, sort them, collect into a LinkedHashMap.
|
||||
Map<String, Integer> 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<String, Integer> byValueDesc = scores.entrySet().stream()
|
||||
.sorted(Map.Entry.<String, Integer>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<String, Integer> 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<Map.Entry<String, Integer>> list = new ArrayList<>(scores.entrySet());
|
||||
list.sort(Map.Entry.comparingByValue(Comparator.reverseOrder()));
|
||||
System.out.println("List<Entry> : " + list);
|
||||
|
||||
// Tie-break: value descending, then key ascending, so equal values come out in a stable, readable order.
|
||||
Map<String, Integer> tieBroken = scores.entrySet().stream()
|
||||
.sorted(Map.Entry.<String, Integer>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);
|
||||
}
|
||||
}
|
||||
@@ -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<Integer, Integer> 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<Map.Entry<Integer, Integer>> sortAllThenLimit() {
|
||||
return TopNDemo.topNBySorting(map, N);
|
||||
}
|
||||
|
||||
@Benchmark
|
||||
public List<Map.Entry<Integer, Integer>> heapTopN() {
|
||||
return TopNDemo.topN(map, N);
|
||||
}
|
||||
|
||||
@Benchmark
|
||||
public Map<Integer, Integer> sortAllIntoLinkedHashMap() {
|
||||
return map.entrySet().stream()
|
||||
.sorted(Map.Entry.comparingByValue())
|
||||
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (a, b) -> a, LinkedHashMap::new));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.ankurm.collections;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
public class TieBugDemo {
|
||||
|
||||
public static void main(String[] args) {
|
||||
Map<String, Integer> 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<String, Integer> 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<String, Integer> 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.
|
||||
}
|
||||
}
|
||||
@@ -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 <K, V extends Comparable<? super V>> List<Map.Entry<K, V>> topN(Map<K, V> map, int n) {
|
||||
PriorityQueue<Map.Entry<K, V>> heap = new PriorityQueue<>(Map.Entry.comparingByValue());
|
||||
for (Map.Entry<K, V> 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<Map.Entry<K, V>> result = new ArrayList<>(heap);
|
||||
result.sort(Map.Entry.<K, V>comparingByValue().reversed());
|
||||
return result;
|
||||
}
|
||||
|
||||
/** Top n by sorting everything, then cutting. */
|
||||
static <K, V extends Comparable<? super V>> List<Map.Entry<K, V>> topNBySorting(Map<K, V> map, int n) {
|
||||
return map.entrySet().stream()
|
||||
.sorted(Map.Entry.<K, V>comparingByValue().reversed())
|
||||
.limit(n)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
Map<String, Integer> scores = Scores.sample();
|
||||
System.out.println("sort + limit(3): " + topNBySorting(scores, 3));
|
||||
System.out.println("heap of size 3 : " + topN(scores, 3));
|
||||
}
|
||||
}
|
||||
@@ -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<String, Integer> 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<String, Integer> scores = Scores.sample();
|
||||
Map<String, Integer> 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<String, Integer> scores = Scores.sample();
|
||||
Map<String, Integer> 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<Integer, Integer> 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<String, Integer> 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<String, Integer> m = Scores.sample().entrySet().stream()
|
||||
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
|
||||
assertEquals(HashMap.class, m.getClass());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user