Java HashMap 排序详解:按 Key、Value 排序的几种常见写法
很多人说“给 HashMap 排个序”,但这句话本身其实有点含糊。
因为 HashMap 的核心特性就是无序。这里的“无序”不是“随机”,而是它不承诺迭代顺序。你今天打印出来看上去是这个顺序,明天数据量一变、扩容一次,遍历结果就可能不同。真正需要排序的,通常不是 HashMap 本身,而是这三件事之一:
- 按
key排序后输出 - 按
value排序后输出 - 排序后保留结果的遍历顺序
这也是很多初学者一开始容易绕进去的地方:你以为自己在“排序 HashMap”,其实你是在“排序它的条目视图,然后决定用什么容器承接结果”。
先说结论:HashMap 不能直接排序,但可以排序它的条目
最常见的做法是:
- 取出
entrySet() - 转成
List或Stream - 根据
key或value排序 - 如果还想保留排序后的顺序,就收集到
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 相同的时候,最好补一个二级排序规则
比如 Jerry 和 Bob 都是 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 排序这件事就不会再绕。