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:
@@ -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