在Java开发中,Map是最常用的集合类型之一。当我们既需要键也需要值时,通常有两种主流遍历方式:通过keySet拿到所有键再取 value,或者通过entrySet直接拿到键值对。二者在写法上差异不大,但在数据量增长时性能表现截然不同。

一、keySet与entrySet的基本用法
keySet方法返回Map中所有键的Set视图,遍历这个Set得到每一个key,再通过Map的get方法获取对应的value。这种方式代码直观,初学者最容易写出。下面的示例展示了如何使用keySet遍历HashMap:
import java.util.HashMap;
import java.util.Map;
public class KeySetDemo {
public static void main(String[] args) {
Map<String, Integer> map = new HashMap<>();
map.put("apple", 1);
map.put("banana", 2);
// 通过keySet遍历
for (String key : map.keySet()) {
Integer value = map.get(key);
System.out.println(key + "=" + value);
}
}
}
entrySet方法返回的是键值对(Map.Entry)的Set视图,每一次迭代直接拿到一个包含key和value的对象,不需要二次查找。下面是用entrySet遍历的等价写法:
import java.util.HashMap;
import java.util.Map;
public class EntrySetDemo {
public static void main(String[] args) {
Map<String, Integer> map = new HashMap<>();
map.put("apple", 1);
map.put("banana", 2);
// 通过entrySet遍历
for (Map.Entry<String, Integer> entry : map.entrySet()) {
String key = entry.getKey();
Integer value = entry.getValue();
System.out.println(key + "=" + value);
}
}
}
从代码可读性来看,entrySet虽然多写了一点entry.getKey()和entry.getValue(),但语义更明确:每一次循环就是处理一个完整的映射关系。而keySet在循环体内隐藏了一次get调用,阅读代码时容易忽略它的存在。
二、底层原理与性能差异分析
要理解性能差异,需要看HashMap的源码实现。HashMap的keySet视图本质上是一个内部类KeySet,它的迭代器遍历的是HashMap的table数组中的节点。当你调用map.get(key)时,HashMap需要根据key的hash重新定位桶位置,并可能遍历链表或红黑树来比对key。也就是说,keySet遍历中每取一个value,就额外做了一次O(1)但带常数开销的查找。
而entrySet返回的EntrySet视图,其迭代器直接返回节点自身(因为HashMap.Node本身实现了Map.Entry),节点里已经保存了key和value的引用。遍历时不需要再次哈希定位,直接读取字段即可。在十万甚至百万级数据下,keySet方式会比entrySet多出十万次以上的get操作,累积的查找和函数调用开销非常明显。
我们可以用一段简单的基准测试代码来直观对比(这里用System.currentTimeMillis粗略计时,真实场景建议用JMH):
import java.util.HashMap;
import java.util.Map;
public class PerformanceTest {
public static void main(String[] args) {
Map<Integer, Integer> map = new HashMap<>();
for (int i = 0; i < 100000; i++) {
map.put(i, i);
}
long start1 = System.currentTimeMillis();
for (Integer key : map.keySet()) {
Integer v = map.get(key);
}
long end1 = System.currentTimeMillis();
System.out.println("keySet耗时: " + (end1 - start1) + "ms");
long start2 = System.currentTimeMillis();
for (Map.Entry<Integer, Integer> entry : map.entrySet()) {
Integer k = entry.getKey();
Integer v = entry.getValue();
}
long end2 = System.currentTimeMillis();
System.out.println("entrySet耗时: " + (end2 - start2) + "ms");
}
}
在我的本地环境多次运行中,keySet通常比entrySet慢30%到100%不等,数据量越大差距越稳定。如果Map的实现是TreeMap,get操作还是O(log n),keySet的劣势会被进一步放大。因此在任何需要同时用key和value的场合,entrySet都是更优解。
三、只遍历键或只遍历值的特殊情况
如果你的业务逻辑确实只需要key,比如检查某些键是否存在、或者仅对key做处理,那么使用keySet是合理的,此时没有多余的get调用,和entrySet比没有性能差距。类似地,如果只需要value,应当使用values()方法而不是通过keySet再get。
import java.util.HashMap;
import java.util.Map;
public class ValuesDemo {
public static void main(String[] args) {
Map<String, Integer> map = new HashMap<>();
map.put("a", 1);
map.put("b", 2);
// 只遍历值
for (Integer value : map.values()) {
System.out.println(value);
}
}
}
很多开发者习惯性地写for (String k : map.keySet()) { ... map.get(k) ... },即使只需要key也会顺手把value取出来,或者反过来只需要value却用entrySet。这种写法虽然不会出错,但遵循最小需求原则能让代码意图更清晰,也避免后续维护者误以为value被使用了。
四、Java 8之后的遍历新写法
从Java 8开始,Map提供了forEach方法,内部其实也是基于entrySet实现的,语法更加简洁。它接受一个BiConsumer,同时传入key和value,既高效又易读。
import java.util.HashMap;
import java.util.Map;
public class ForEachDemo {
public static void main(String[] args) {
Map<String, Integer> map = new HashMap<>();
map.put("x", 10);
map.put("y", 20);
// Java8 forEach,底层用entrySet
map.forEach((k, v) -> System.out.println(k + "=" + v));
}
}
如果需要在遍历时做并行处理,可以使用parallelStream,但注意entrySet的并行流底层依旧是拆分entrySet的视图,不会产生额外的get开销。相比之下,若有人误用keySet的并行流并在内部get,不仅慢还可能因HashMap非线程安全引发问题。因此无论串行还是并行,优先选择entrySet或forEach。
五、总结与选型建议
综合来看,当需要同时访问Map的键和值时,entrySet以及基于它的forEach是性能最好、语义最清晰的方案;keySet仅适合只处理键的场景,切忌在循环体内配合get使用。在代码评审中,可以将keySet加get的写法列为不规范用法。理解这一差异不仅能避免无谓的性能损耗,也体现了对集合底层实现的掌握程度。
对于遗留系统里大量存在的keySet遍历代码,如果Map数据量较小(比如配置项、常量映射),重构优先级可以放低;但如果是热点接口中遍历大Map,应尽快改为entrySet。这样简单的改动往往能带来可观的响应时间提升。
JavaMap遍历entrySet_keySet修改时间:2026-08-03 20:15:38