原创

Java HashMap 排序详解:按 Key、Value 排序的几种常见写法

很多人说“给 HashMap 排个序”,但这句话本身其实有点含糊。

因为 HashMap 的核心特性就是无序。这里的“无序”不是“随机”,而是它不承诺迭代顺序。你今天打印出来看上去是这个顺序,明天数据量一变、扩容一次,遍历结果就可能不同。真正需要排序的,通常不是 HashMap 本身,而是这三件事之一:

  1. key 排序后输出
  2. value 排序后输出
  3. 排序后保留结果的遍历顺序

这也是很多初学者一开始容易绕进去的地方:你以为自己在“排序 HashMap”,其实你是在“排序它的条目视图,然后决定用什么容器承接结果”。

先说结论:HashMap 不能直接排序,但可以排序它的条目

最常见的做法是:

  • 取出 entrySet()
  • 转成 ListStream
  • 根据 keyvalue 排序
  • 如果还想保留排序后的顺序,就收集到 LinkedHashMap

这一步很关键。因为你就算把条目排好了,最后又塞回 HashMap,顺序还是保不住。

先准备一份测试数据:

import java.util.*;

public class HashMapSortDemo {
    public static void main(String[] args) {
        Map<String, Integer> scoreMap = new HashMap<>();
        scoreMap.put("Tom", 85);
        scoreMap.put("Jerry", 92);
        scoreMap.put("Alice", 78);
        scoreMap.put("Bob", 92);

        System.out.println(scoreMap);
    }
}

按 key 排序:最省事的办法其实是 TreeMap

如果你的需求是按 key 的自然顺序排序,而且后续还会继续按这个顺序访问,那么 TreeMap 往往比“先排序再收集”更直接。

方式一:直接放入 TreeMap

Map<String, Integer> scoreMap = new HashMap<>();
scoreMap.put("Tom", 85);
scoreMap.put("Jerry", 92);
scoreMap.put("Alice", 78);
scoreMap.put("Bob", 92);

Map<String, Integer> sortedByKey = new TreeMap<>(scoreMap);

for (Map.Entry<String, Integer> entry : sortedByKey.entrySet()) {
    System.out.println(entry.getKey() + " : " + entry.getValue());
}

输出顺序会按 key 升序排列:

Alice : 78
Bob : 92
Jerry : 92
Tom : 85

为什么 TreeMap 适合按 key 排序

因为 TreeMap 底层是红黑树,它维护的是有序键集合。你不是“排一次序”,而是从结构层面就选择了“有序 Map”。

这适合下面这种场景:

  • 数据需要长期按 key 有序访问
  • 需要范围查询,比如 subMap()headMap()
  • 不只是打印一次,而是后续逻辑也依赖顺序

但它也不是白来的。HashMap 的常见操作平均是 O(1)TreeMap 通常是 O(log n)。如果你只是临时导出一下排序结果,直接换成 TreeMap 可能有点重。

按 key 排序后保留顺序:List + LinkedHashMap 更灵活

如果你只是想对现有 HashMap 做一次排序展示,或者需要自定义排序规则,那通常会走这一套:

import java.util.*;
import java.util.stream.Collectors;

public class SortByKeyExample {
    public static void main(String[] args) {
        Map<String, Integer> scoreMap = new HashMap<>();
        scoreMap.put("Tom", 85);
        scoreMap.put("Jerry", 92);
        scoreMap.put("Alice", 78);
        scoreMap.put("Bob", 92);

        Map<String, Integer> sortedMap = scoreMap.entrySet()
                .stream()
                .sorted(Map.Entry.comparingByKey())
                .collect(Collectors.toMap(
                        Map.Entry::getKey,
                        Map.Entry::getValue,
                        (oldValue, newValue) -> oldValue,
                        LinkedHashMap::new
                ));

        System.out.println(sortedMap);
    }
}

这里有两个细节经常被忽略:

1. sorted() 排的是流,不是原始 HashMap

原始的 scoreMap 并没有变成“有序 HashMap”。你只是得到了一个新的有序结果。

2. 一定要用 LinkedHashMap::new

因为 Collectors.toMap() 默认收集成什么,不一定是你想要的。 如果你不显式指定 LinkedHashMap,辛辛苦苦排好的顺序,最后可能又没了。

按 value 排序:这是更常见也更容易踩坑的需求

key 排序还能靠 TreeMap,按 value 排序就不行了。因为 Map 的结构天然是围绕 key 建立的,value 只是附属信息。

这时候最稳妥的方式就是排序 entrySet()

按 value 升序排序

import java.util.*;
import java.util.stream.Collectors;

public class SortByValueExample {
    public static void main(String[] args) {
        Map<String, Integer> scoreMap = new HashMap<>();
        scoreMap.put("Tom", 85);
        scoreMap.put("Jerry", 92);
        scoreMap.put("Alice", 78);
        scoreMap.put("Bob", 92);

        Map<String, Integer> sortedByValue = scoreMap.entrySet()
                .stream()
                .sorted(Map.Entry.comparingByValue())
                .collect(Collectors.toMap(
                        Map.Entry::getKey,
                        Map.Entry::getValue,
                        (oldValue, newValue) -> oldValue,
                        LinkedHashMap::new
                ));

        System.out.println(sortedByValue);
    }
}

输出结果类似:

{Alice=78, Tom=85, Jerry=92, Bob=92}

按 value 降序排序

Map<String, Integer> sortedByValueDesc = scoreMap.entrySet()
        .stream()
        .sorted(Map.Entry.<String, Integer>comparingByValue().reversed())
        .collect(Collectors.toMap(
                Map.Entry::getKey,
                Map.Entry::getValue,
                (oldValue, newValue) -> oldValue,
                LinkedHashMap::new
        ));

这已经够用了,但实际项目里还有一个麻烦:value 相同时怎么办?

value 相同的时候,最好补一个二级排序规则

比如 JerryBob 都是 92 分。如果你只按 value 排序,那么它们之间的先后次序不够直观。很多时候我们会希望在 value 相同的情况下,再按 key 排一遍。

Map<String, Integer> sortedMap = scoreMap.entrySet()
        .stream()
        .sorted(
                Map.Entry.<String, Integer>comparingByValue().reversed()
                        .thenComparing(Map.Entry.comparingByKey())
        )
        .collect(Collectors.toMap(
                Map.Entry::getKey,
                Map.Entry::getValue,
                (oldValue, newValue) -> oldValue,
                LinkedHashMap::new
        ));

System.out.println(sortedMap);

这段代码的意思是:

  • 先按 value 降序
  • 如果 value 一样,再按 key 升序

这种写法在排行榜、统计报表里很常见。否则结果虽然“能跑”,但看起来总有点别扭。

不用 Stream 行不行?可以,而且有时候更直白

有些人看 Stream 会觉得写法优雅,但调试不顺手。尤其在老项目里,或者你只是想让逻辑一眼能看明白,直接用 List 排序反而更实在。

传统写法:先转 List,再排序

import java.util.*;

public class SortWithListExample {
    public static void main(String[] args) {
        Map<String, Integer> scoreMap = new HashMap<>();
        scoreMap.put("Tom", 85);
        scoreMap.put("Jerry", 92);
        scoreMap.put("Alice", 78);
        scoreMap.put("Bob", 92);

        List<Map.Entry<String, Integer>> entryList = new ArrayList<>(scoreMap.entrySet());

        entryList.sort((e1, e2) -> {
            int valueCompare = e2.getValue().compareTo(e1.getValue()); // 降序
            if (valueCompare != 0) {
                return valueCompare;
            }
            return e1.getKey().compareTo(e2.getKey()); // value 相同按 key 升序
        });

        Map<String, Integer> resultMap = new LinkedHashMap<>();
        for (Map.Entry<String, Integer> entry : entryList) {
            resultMap.put(entry.getKey(), entry.getValue());
        }

        System.out.println(resultMap);
    }
}

这套写法虽然长一点,但有几个优点:

  • 调试方便
  • 排序逻辑容易拆开看
  • 对不熟 Stream 的同事更友好

很多团队代码最后会在“简洁”和“可维护”之间选后者,这很正常。

几种排序方式该怎么选

下面这张表可以快速判断该用哪种方案:

需求 推荐方案 原因
按 key 长期保持有序 TreeMap 容器本身就是有序的
对现有 HashMap 按 key 临时排序 Stream + LinkedHashMap 简洁,输出顺序可保留
按 value 排序 entrySet() + sorted() + LinkedHashMap 这是最常规也最稳定的方案
排序逻辑复杂,想方便调试 List<Map.Entry> + sort() 更直白,便于维护

常见误区,比语法本身更值得注意

误区一:以为 HashMap 遍历“看起来有序”就是有序

这是最典型的错觉。

你在本地测试时,HashMap 输出顺序可能连续几次都一样,于是误以为它“其实也是有规律的”。但那只是当前哈希分布、容量、扩容状态下的表现,不是契约。

只要你依赖这种顺序,迟早会出问题。

误区二:排序完又放回 HashMap

这个错误不算少见,代码大概长这样:

Map<String, Integer> sortedMap = new HashMap<>();
for (Map.Entry<String, Integer> entry : sortedEntries) {
    sortedMap.put(entry.getKey(), entry.getValue());
}

这样做基本等于白排。 因为你最终还是把结果交给了一个不保证顺序的容器。

误区三:按 value 排序时忽略相同值的处理

如果多个元素的 value 一样,而你又没有补充排序规则,那么结果顺序可能不够稳定,也不够易读。 报表类功能最容易在这里翻车,看上去像“小问题”,实际用户一眼就能看出来排序不自然。

误区四:为了一次导出,把整个结构都换成 TreeMap

TreeMap 不是不能用,而是要看场景。 如果你只是想“最后展示前排一下序”,没必要把中间所有写入、查找逻辑都改成 TreeMap。那通常是为了一个局部需求,付出了全局性能和复杂度的代价。

一个更贴近实际项目的封装方式

如果项目里经常要做“按 value 排序”,可以直接封装一个工具方法,避免每次都复制一长段流操作。

import java.util.*;
import java.util.stream.Collectors;

public class MapSortUtil {

    public static <K extends Comparable<? super K>, V extends Comparable<? super V>>
    Map<K, V> sortByValueDescThenKeyAsc(Map<K, V> map) {
        return map.entrySet()
                .stream()
                .sorted(
                        Map.Entry.<K, V>comparingByValue().reversed()
                                .thenComparing(Map.Entry.comparingByKey())
                )
                .collect(Collectors.toMap(
                        Map.Entry::getKey,
                        Map.Entry::getValue,
                        (oldValue, newValue) -> oldValue,
                        LinkedHashMap::new
                ));
    }

    public static void main(String[] args) {
        Map<String, Integer> scoreMap = new HashMap<>();
        scoreMap.put("Tom", 85);
        scoreMap.put("Jerry", 92);
        scoreMap.put("Alice", 78);
        scoreMap.put("Bob", 92);

        Map<String, Integer> result = sortByValueDescThenKeyAsc(scoreMap);
        System.out.println(result);
    }
}

这种封装的价值不只是少写几行代码,而是把“排序后的结果必须保留顺序”这个细节固定下来。否则不同人各写各的,很容易有人最后又收集回 HashMap

什么时候不该执着于“给 HashMap 排序”

有些需求从源头上就不该用 HashMap 来承接。

比如:

  • 你明确需要按插入顺序遍历 —— 用 LinkedHashMap
  • 你明确需要按 key 有序 —— 用 TreeMap
  • 你只是做一次统计后导出 —— 用 HashMap 统计,最后单独排序输出

说白了,HashMap 适合做快速存取,不适合扮演“有序结果集”。 把它用在不擅长的地方,代码就会开始变拧巴。

总结

“Java HashMap 排序”这件事,真正要搞清楚的不是 API,而是思路:

  • HashMap 本身不保证顺序
  • 排序对象通常是 entrySet()
  • 排完序如果还想保留结果顺序,要用 LinkedHashMap
  • 按 key 排序,TreeMap 往往更合适
  • 按 value 排序,通常只能走 entrySet + sort
  • value 相同的场景,最好补二级排序规则

很多代码看上去是在“操作集合”,实际做的是“表达业务意图”。 你只是想打印一下排行榜,和你想维护一个长期有序的数据结构,这不是一回事。把这两类需求分开,HashMap 排序这件事就不会再绕。

正文到此结束
评论插件初始化中...
Loading...