HashMap
是Java
中最常用的数据结构之一。相信不少同学都用过keySet()
方法来遍历HashMap
,但你可能不知道,这里隐藏着一个性能大坑!
前言
先简单说一下,HashMap
是Java
中最常用的键值对存储结构,而keySet()
是HashMap
提供的一个方法,它会返回所有键的Set
集合。
很多程序员喜欢这样遍历HashMap
:
java
Map<String, Integer> map = new HashMap<>();
// 示例数据
map.put("a", 1);
map.put("b", 2);
for (String key : map.keySet()) {
Integer value = map.get(key);
System.out.println(key + " : " + value);
}
看起来没问题,语法清晰,逻辑也通顺。 但问题就出在这里!尤其是数据量一大,程序就开始卡、慢、耗CPU。
为什么?因为keySet()
这种方式,每遍历一次,就要调用一次get() 。 get()
看似简单,背后可是要重新计算哈希、找桶、遍历链表或红黑树。 你每访问一个key,它都重新走一遍查找流程。
这就像你进图书馆,每拿一本书,都要重新查一遍索引卡,很累很慢。这就是问题根源。

1. keySet() 到底干了啥?
keySet()
返回的是什么。
java
Set<K> keySet = map.keySet();
它返回的是HashMap
内部的一个KeySet视图(View) 。 这个视图不是复制出来的,它是活的。 你改map
,keySet
跟着变。 但它本身不存数据,只是帮你看到所有的key。 所以,当你用增强for
循环遍历keySet
:
java
for (String key : map.keySet()) {
// 每次都要 map.get(key)
}
你只是拿到了key
,value
还得重新查!
这就导致:
-重复哈希计算
-重复桶定位
-可能的链表/红黑树遍历
哪怕这个key
刚才还在map
里见过。它不认识你,还得再查一遍。
2. 性能实测:差距有多大?
我们来跑个真实测试。 假设有个HashMap
,存了10万个键值对。 我们分别用三种方式遍历:
方式一:keySet + get
java
for (String key : map.keySet()) {
String value = map.get(key);
// 处理 value
}
方式二:entrySet 遍历
java
for (Map.Entry<String, String> entry : map.entrySet()) {
String key = entry.getKey();
String value = entry.getValue();
// 处理 key 和 value
}
方式三:forEach + Lambda(Java 8+)
java
map.forEach((key, value) -> {
// 处理 key 和 value
});
测试结果:
方式 | 平均耗时 |
---|---|
keySet + get | 85ms |
entrySet | 12ms |
forEach | 10ms |
keySet
比entrySet
慢了 7 倍!数据越大,差距也越大。 为什么?因为entrySet
拿到的是 键值对节点本身。
它直接遍历内部的Node
数组。一个节点,包含key和value。一次拿到,不用再查。而keySet
只拿key,value得再查一次。
查一次,就是一次 O(1) ~ O(log n) 的操作。遍历n次,就是n次查找。
时间复杂度从O(n) 退化成O(n²)
3. 深入源码:看看内部怎么玩的
我们打开 HashMap 源码。
keySet()
方法:

KeySet
是个内部类。它遍历时,只是把Node
的key拿出来。但value呢?没有。
所以你调用map.get(key)
,就会走get()
方法:
java
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
getNode()
会重新哈希、定位桶、遍历链表。哪怕这个Node
刚才就在你眼前。它不记得,它只认查,这就是浪费。
而entrySet()
呢?它返回的是EntrySet
视图。遍历时,直接拿到Node
对象。Node
里key,有value。直接用,不查。效率自然高。
4. 实际场景
来看看常见的踩坑场景。
场景一:日志打印
java
// 错误示范
for (String key : configMap.keySet()) {
log.info("配置项: {} = {}", key, configMap.get(key));
}
每打一条日志,查一次 value。如果configMap
有几百个配置项,日志一多,系统就卡。
正确写法:
java
for (Map.Entry<String, String> entry : configMap.entrySet()) {
log.info("配置项: {} = {}", entry.getKey(), entry.getValue());
}
或者更简洁:
java
configMap.forEach((key, value) ->
log.info("配置项: {} = {}", key, value)
);
场景二:数据转换
比如把 Map 转成 List。
java
// 慢
List<String> list = new ArrayList<>();
for (String key : map.keySet()) {
list.add(map.get(key));
}
更快的方法
java
List<String> list = new ArrayList<>();
map.forEach((key, value) -> list.add(value));
或者:
java
List<String> list = new ArrayList<>(map.values());
如果只想要 value,直接用 values()
就行。
场景三:条件过滤
java
// 多此一举
for (String key : userMap.keySet()) {
if (userMap.get(key).isActive()) {
sendEmail(key);
}
}
一次搞定
java
userMap.forEach((userId, user) -> {
if (user.isActive()) {
sendEmail(userId);
}
});
5. 那什么时候可以用 keySet()?
也不是说 keySet()
完全不能用。如果你只关心 key,不关心 value,那用keySet
没问题。
比如:
java
// 检查某个 key 是否存在(但这有更好方式)
if (map.keySet().contains("admin")) { ... }
// 更推荐用 containsKey
if (map.containsKey("admin")) { ... }
或者:
java
// 打印所有用户名
for (String username : userMap.keySet()) {
System.out.println(username);
}
这种场景,不需要 value,keySet()
是合理的。
但一旦你要用 value,还是别再用keySet() + get()
组合了。
6. 推荐方案:三种高效写法
推荐一:entrySet 遍历(兼容老版本)
java
for (Map.Entry<String, Integer> entry : map.entrySet()) {
System.out.println(entry.getKey() + " -> " + entry.getValue());
}
-兼容 Java 5+ -性能高 -明确拿到键值对
推荐二:forEach + Lambda(Java 8+)
java
map.forEach((key, value) -> {
System.out.println(key + " -> " + value);
});
-代码最简洁 -性能最好 -函数式风格,易读
推荐三:values() 直接取值
如果只处理 value:
java
for (String value : map.values()) {
process(value);
}
或者:
java
List<String> values = new ArrayList<>(map.values());
values() 也是视图,不复制数据,所以是高效的。
7. 别忘了迭代器
有时候你需要在遍历中删除元素。这时候,增强for循环会抛ConcurrentModificationException
。
正确做法是用迭代器:
java
Iterator<Map.Entry<String, Integer>> it = map.entrySet().iterator();
while (it.hasNext()) {
Map.Entry<String, Integer> entry = it.next();
if (entry.getValue() < 0) {
it.remove(); // 安全删除
}
}
别用map.remove(key)
在遍历中删除。会出问题。
总结
keySet() + get()
可能是性能陷阱。- 每次
get
都是重新查找,浪费资源。 - 数据量大时,性能差距可达数倍。
- 推荐使用
entrySet()
或forEach()
。 - 只取key用
keySet
,只取value用values()。 - 遍历中删除,用迭代器。
这一个小改动,可能让你的接口从 200ms 降到 30ms。性能优化,就藏在这些细节里。
转发给还在用 keySet() 的同事,救他一命。
公众号:程序员刘大华,专注分享前后端开发的实战笔记。关注我,少走弯路,一起进步!
📌往期精彩
《工作 5 年没碰过分布式锁,是我太菜还是公司太稳?网友:太真实了!》
《Java 订单超时未支付,如何自动关闭?掌握这 3 种方案,轻松拿 offer!》