Skip to main content

How to Sort a HashMap by Value (and Key) in Java

The shortest correct way to sort a Java HashMap by value or key (stream into a LinkedHashMap), plus the traps: a TreeMap with a value comparator silently drops tied entries, top-N with a PriorityQueue measured against sort-and-limit in JMH, and JDK 21+ sequenced collections (reversed, firstEntry).

You have a HashMap<String, Integer> of scores, word counts or prices, and you want it ordered by the numbers, not by the keys. The shortest correct answer is one stream: entrySet().stream().sorted(Map.Entry.comparingByValue()).collect(...) into a LinkedHashMap. The rest of this article is the part that answers usually skip: why a HashMap cannot be sorted in place, why a TreeMap with a value comparator silently deletes entries whenever two values are equal, how to take only the top N without sorting everything (with a measured comparison), and what the sequenced collections added in JDK 21 give you for free.
Versions. JDK 25.0.4.1+1 (Temurin, LTS), JMH 1.37, JUnit Jupiter 5.11.0, run on a 2-vCPU x86-64 virtual machine that was shared with other jobs. Every code block and every line of output below comes from the collections module of the java-core-examples repository. The timings show ordering on that machine, not a leaderboard. The sequenced-collection methods were run on JDK 25 only; that they arrived in JDK 21 comes from JEP 431, not from a run on 21.

A HashMap has no order to sort, so you build a new ordered map

A HashMap does not keep entries in any order you control; it iterates in whatever order its internal buckets happen to produce. So there is no sort() on a map, and Collections.sort(map) does not even compile (the exact javac message is in the reference section below). The working recipe is to take the entries out, sort those, and pour them into a map that remembers insertion order. That map is LinkedHashMap. The demo map has five scores, with bob and dave deliberately tied at 70 (SortByValueDemo.java):
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);
The output shows the original HashMap order and the sorted result (01-sort-by-value.txt):
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}
HashMapno usable orderentrySet().stream()entries flow outsorted(comparingByValue())collect intoLinkedHashMapcollect into HashMaporder lost againSorting happens on the stream. Only a map that remembers insertion order can keep the result.
The diagram is the whole technique. The stream does the sorting; the LinkedHashMap is only there to keep the result in that order. The fourth argument to toMap (LinkedHashMap::new) is the part people leave out, and the (a, b) -> a before it is a merge function that is required by that four-argument overload even though entries from a map never collide.
The fingerprint of a forgotten LinkedHashMap::new. Collect with the two-argument Collectors.toMap(...) and you get a plain HashMap back, so the sorted order is gone the moment you collect. The demo does this on purpose and prints the class (01-sort-by-value.txt):
toMap() no LHM: {carol=85, dave=70, bob=70, alice=90, erin=95}  (class HashMap)
Notice that the map printed in its original scrambled order. If a sorted result “doesn’t stay sorted”, this is the first thing to check.

Sorting by key is easier, and descending just flips the comparator

To order by key, use a TreeMap: new TreeMap<>(scores) copies the map and keeps it sorted by key from then on. That is line 4 of the same transcript (01-sort-by-value.txt):
by key (Tree) : {alice=90, bob=70, carol=85, dave=70, erin=95}
For descending value order, reverse the comparator. Java needs a type hint on comparingByValue here (Map.Entry.<String, Integer>comparingByValue()) because the call is chained into .reversed(); there is an alternative that needs no hint, comparingByValue(Comparator.reverseOrder()). Both are in the demo, along with the case where you do not need a map at all: if you only want to loop in order, copy the entries into a List and sort that (SortByValueDemo.java):
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);
// 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);
by value desc : {erin=95, alice=90, carol=85, dave=70, bob=70}
...
List<Entry>   : [erin=95, alice=90, carol=85, dave=70, bob=70]
A List<Map.Entry<...>> is often the better result type. It has no key-uniqueness rule to trip over and no hashing cost, and it is what you want for a leaderboard table or a JSON array. The catch is that looking up a value by key is no longer cheap, so keep the original map for lookups if you need them. For how a HashMap behaves underneath (and why its order is arbitrary), see Java HashMap vs ConcurrentHashMap: Complete Interview Guide. Going deeper

Equal values: a TreeMap with a value comparator silently drops entries

Here is the most common wrong answer, and it works on test data until two values match. Someone reasons: “TreeMap sorts, so give it a comparator that compares values.” The code below does that (TieBugDemo.java):
Map<String, Integer> byValue = new TreeMap<>(Comparator.comparing(scores::get));
byValue.putAll(scores);
System.out.println("TreeMap(value): " + byValue + "  size " + byValue.size());
It compiles, it runs, it throws nothing, and it loses data (02-tie-bug.txt):
input size    : 5
TreeMap(value): {dave=70, carol=85, alice=90, erin=95}  size 4
containsKey    : bob true, dave true  get(bob) = 70
putAll() inserts entries one at a time; the comparator decides whether a key is ‘new’put(dave, 70)storedput(bob, 70)compare(bob, dave) = 0‘same key’ -> value replacedno new slot is createdTreeMap afterwards{dave=70, carol=85,alice=90, erin=95}size 4, input was 5TreeMap uses the comparator for equality, not equals(). Equal values therefore mean equal keys.The surviving name (dave here) depends on HashMap iteration order, so it is not something to rely on.
The diagram shows why. A TreeMap has no idea what equals() says about keys; it treats two keys as the same when its comparator returns 0. Our comparator looks only at the score, so bob (70) and dave (70) are the same key. putAll goes through the HashMap in its own iteration order, dave arrives first, and bob then overwrites the same slot. Nothing is thrown, the size quietly drops from 5 to 4, and which of the two tied names survives depends on the order of the source map.
It gets worse: lookups lie too. The second line of the transcript shows containsKey("bob") and containsKey("dave") both returning true, and get("bob") returning 70, even though one of those two names is not in the map. Lookups go through the same comparator, so a TreeMap with this comparator answers “is there anything that compares equal?” (02-tie-bug.txt). If you test only with distinct values you will never see this.
There are two correct fixes. If you want a map, make the comparator total by breaking ties with the key; the TreeMap then keeps all five entries (TieBugDemo.java):
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());
TreeMap fixed : {bob=70, dave=70, carol=85, alice=90, erin=95}  size 5
Or, simpler and safer, skip the TreeMap and use the stream from the first section with an explicit tie-break. The comparator below sorts by value descending, then by key ascending, so equal scores come out in a predictable order. Without a tie-break the stream version never loses entries, but equal values appear in the source map’s iteration order, which is arbitrary (SortByValueDemo.java):
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));
value desc+key: {erin=95, alice=90, carol=85, bob=70, dave=70}
Compare it with line 3 of the same transcript, where dave came before bob; with the tie-break, bob comes first every time. A second reason to prefer the stream: a TreeMap whose comparator reads from another map (as scores::get does) breaks without any error the moment a score changes, because the tree is not re-sorted. Here the values sit inside the sorted copy, so nothing can go stale.
Reference: what javac says when you try to sort a map directly, and the test that pins the tie bug Collections.sort takes a List, not a Map (SortMapDirectly.java, output 06-SortMapDirectly-compile-error.txt):
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
The tie bug is pinned by valueOnlyTreeMapComparatorDropsTies and tieBreakingComparatorKeepsEverything in SortingTest; all six tests pass (08-tests.txt):
Tests run: 6, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.238 s -- in com.ankurm.collections.SortingTest
Going deeper

Top N: do not sort a hundred thousand entries to keep ten

A leaderboard rarely needs the whole map sorted; it needs the top ten. The obvious code is sorted(...).limit(10), which sorts every entry and then throws almost all of that work away. The alternative is a min-heap of size N (Java’s PriorityQueue): for each entry, if the heap has room add it, otherwise compare with the smallest entry in the heap and replace it only when the new one is bigger. One pass, and the heap never holds more than N entries. Both are in TopNDemo.java:
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;
}
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());
}
sort + limit(3): [erin=95, alice=90, carol=85]
heap of size 3 : [erin=95, alice=90, carol=85]
They agree on the small sample above, and a test compares them on 5,000 random entries (SortingTest, heapAndSortAgreeOnTopN). The benchmark measures both for a top-10 from a HashMap of 1,000 and of 100,000 entries, plus the full sort into a LinkedHashMap for reference (SortingBenchmark.java):
Average time per call, microseconds (log scale) — JMH, JDK 25, 2 vCPUheap top-10 n=1,00010.3 usheap top-10 n=100,000642 ussort+limit(10) n=1,00055.6 ussort+limit(10) n=100,00035128 ussort into LHM n=1,000122 ussort into LHM n=100,00046583 usBars are on a log scale; raw scores are in collections/output/07-jmh-raw.txt
At 1,000 entries the heap took about 10 us against about 56 us for sort-then-limit, roughly 5x faster. At 100,000 entries it was about 642 us against about 35128 us, roughly 55x faster. That is the expected shape: sorting is O(n log n) and a size-N heap is O(n log N), and with N fixed at 10 the heap’s per-entry work barely grows. Sorting the complete map into a LinkedHashMap cost about 122 us and 46583 us, because it also allocates a new map and every entry. The raw scores are in 07-jmh-raw.txt:
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
Read the error column before quoting these numbers. The machine was shared with other jobs while this ran (I saw another benchmark process at the start), and the 100,000-entry sort rows have wide confidence intervals (the Error column is JMH’s 99.9% half-interval; sortAllThenLimit at 100,000 is 35128 +/- 10045 us). I used two forks and longer iterations to tighten them. Treat the ratios as “several times” and “dozens of times”, not as precise multipliers.
Reference: how this was measured and what it does not cover Two forks, three warm-up and five measurement iterations of two seconds each, average-time mode, keys 0 to n-1 and random int values from a seeded Random(42). The annotation-processor setting in the module’s pom.xml is required on JDK 25, which no longer discovers processors implicitly. Regenerate everything with scripts/run-all.sh (needs JDK25_HOME). Not covered: other N values, other key/value types, and parallel streams. The benchmark does not measure memory. Random values make ties rare, so this says nothing about how the heap behaves on heavily tied data. Note the heap version as written keeps the first of several equal values it sees at the cutoff, which is a fine behaviour but different from the stable sort.
Going deeper

On JDK 21 and later, a sorted LinkedHashMap can be read from either end

Before JDK 21, asking a LinkedHashMap for its last entry meant iterating the whole thing. JEP 431 added the SequencedMap interface, implemented by LinkedHashMap and TreeMap, with firstEntry(), lastEntry(), putFirst(), putLast(), pollFirstEntry() and, most useful here, reversed(). Once you have a map sorted ascending, its reversed() view is the descending map, with no second sort. The demo (SequencedDemo.java):
// 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);
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]
HashMapnot sequencedno firstEntry(), no reversed()LinkedHashMapsequenced: insertion orderyour sorted result lives hereTreeMapsequenced: key ordercomparator decides equalitySequencedMap methodsfirstEntry lastEntry reversed putFirst putLast pollFirstEntryJDK 21+ only: on older JDKs these methods do not exist on LinkedHashMap.
The diagram says which types you can call these methods on. A HashMap is not one of them: the compile error for m.firstEntry() on a HashMap is captured below. reversed() returns a live view, not a copy, which the next lines of the transcript show: a put through the reversed view shows up in the original map (04-sequenced.txt):
after put via view (no longer sorted): {bob=70, carol=85, alice=90, erin=95, zed=60}
putLast/putFirst  : {carol=85, alice=90, erin=95, zed=60, bob=70}
TreeMap first : alice=90  reversed: {zed=60, erin=95, carol=85, bob=70, alice=90}
pollFirstEntry: carol=85 -> {alice=90, erin=95, zed=60, bob=70}
A view is not a snapshot, and a put does not re-sort. The entry added through the view landed at the end of the map, so the map is no longer sorted by value (zed=60 sits after erin=95). Adding to a LinkedHashMap never re-sorts it; putLast and putFirst move a key to an end. If you keep mutating a map, re-sort from the stream rather than trusting old order.
Reference: the HashMap compile error, and TreeMap as a SequencedMap Calling firstEntry() on a HashMap-typed variable does not compile (HashMapIsNotSequenced.java, output 05-HashMapIsNotSequenced-compile-error.txt):
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
TreeMap is sequenced by key order, so tree.reversed() gives you keys from high to low without a custom comparator; the last lines of 04-sequenced.txt show it:
TreeMap first : alice=90  reversed: {zed=60, erin=95, carol=85, bob=70, alice=90}
The pollFirstEntry() call in the demo removes and returns the first entry. It is a destructive read, so do not use it to “peek”; use firstEntry().
Going deeper

Which approach should you pick?

You wantUseWatch out for
The whole map ordered by value, then iterate or print itentrySet().stream().sorted(...) into a LinkedHashMap (first section)Forgetting LinkedHashMap::new; no tie-break means tied values appear in arbitrary order
Just to loop in value orderA sorted List<Map.Entry<K,V>>No cheap lookup by key; keep the original map if you need it
The map always ordered by keyTreeMapComparator must be consistent with equals; never a value-only comparator
Top N by valueA size-N PriorityQueue, or sorted().limit(N) if N is close to the map size or the code runs rarelyHeap code is longer; measure first
Read a sorted map from the other end (JDK 21+)reversed(), firstEntry(), lastEntry() on the LinkedHashMapIt is a view; later puts do not re-sort
Honest advice (my opinion, backed by the measurements above). For a map with a few hundred entries that you sort once, the stream into a LinkedHashMap is clear and fast enough; do not reach for anything cleverer. Reach for the heap when the map is large and you only need the top few, and reach for a different data structure when you sort the same data repeatedly: if you find yourself re-sorting a map after every update, a sorted list, a database ORDER BY, or a structure built for ranking is the real answer. And the one thing to never do is the value-only TreeMap: its failure is silent, and the data you lose is exactly the data that happened to tie.
The checklist that falls out of this: sort the stream, not the map; always collect into a LinkedHashMap; break ties on the key; take the top N with a heap; and on JDK 21 and later, read from either end with reversed() instead of sorting twice.

Further reading

No Comments yet!

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.