【Java基础】HashMap遍历实战笔记:从keySet到entrySet,我踩过的坑与底层原理

前言

在学习 Java 集合框架时,HashMap 的遍历是最基础但也最容易“知其然不知其所以然”的知识点。最近我在练习 HashMap 遍历时,写了两种常见的遍历方式,虽然都能跑通,但在查阅资料后才发现背后藏着不少性能陷阱和设计精髓。

本文以一段实际测试代码为切入点,记录我的学习过程、易错点总结以及对 entrySet() 底层原理的理解,希望能帮到同样在打基础的你。

我的测试代码

@Test
void forMap() {
    HashMap<String, String> map = new HashMap<>();
    map.put("1", "xiaoming");
    map.put("2", "libai");
    map.put("3", "dufu");
    map.put("4", "zhangsan");

    // ========== 方式一:keySet 遍历 ==========
    Set<String> keySet = map.keySet();
    System.out.println(keySet);
    for (String key : keySet) {
        //  每次循环都要额外调用 get()
        System.out.println(map.get(key));
    }

    System.out.println("-".repeat(10));

    // ========== 方式二:entrySet 遍历 ==========
    for (Map.Entry<String, String> entry : map.entrySet()) {
        System.out.println("key=" + entry.getKey());
        System.out.println("value=" + entry.getValue());
    }
}

学习过程:从"能用"到"理解"

第一阶段:只会用 keySet

刚开始学的时候,我觉得 keySet() 很直观——先拿到所有 key,再通过 key 去查 value,逻辑清晰。但很快我就发现一个问题:如果 Map 很大,这种方式明显变慢了

第二阶段:发现 entrySet 更高效

在这里插入图片描述

后来了解到 entrySet() 可以在一次迭代中同时拿到 key 和 value,不需要二次查找。改造代码后发现确实更简洁,但新的疑问来了:

entrySet() 返回的到底是什么?为什么它能直接拿到 key 和 value?

第三阶段:深入理解"视图"概念

查阅源码后才恍然大悟:entrySet() 不是创建了一个新集合,而是返回了原 Map 的一个"实时视图"。它就像一张库存清单,本身不存数据,看到和操作的都是 Map 本身。这也解释了为什么通过 entry.setValue() 能直接修改原 Map。

⚠️ 核心易错点总结

易错点1:keySet 遍历中的 N+1 查询问题

// 错误示范:时间复杂度 O(n) 次哈希查找
for (String key : map.keySet()) {
    String value = map.get(key); // 每次都重新计算 hash、定位桶、遍历链表
}
  • 问题本质get() 需要重新走一遍哈希定位流程
  • 性能影响:当 Map 数据量大时,额外的 n 次哈希开销不可忽视
  • 正确做法:需要同时访问 key 和 value 时,永远优先使用 entrySet()

易错点2:遍历时直接用 map.remove() 导致 ConcurrentModificationException

// 运行时异常!
for (Map.Entry<String, String> entry : map.entrySet()) {
    if ("dufu".equals(entry.getValue())) {
        map.remove(entry.getKey()); // 禁止!
    }
}
  • 原因:增强 for 底层是迭代器,直接调用 Map 的 remove 会修改 modCount,触发 fail-fast 机制
  • 正确做法:使用迭代器的 remove() 方法
Iterator<Map.Entry<String, String>> it = map.entrySet().iterator();
while (it.hasNext()) {
    Map.Entry<String, String> entry = it.next();
    if ("dufu".equals(entry.getValue())) {
        it.remove(); //  安全删除
    }
}

易错点3:误以为 entrySet() 是独立副本

Set<Map.Entry<String, String>> entries = map.entrySet();
map.put("5", "lisi");
System.out.println(entries.size()); // 5!不是4!
  • 认知纠正entrySet()实时视图,不是快照。原 Map 变了,视图自动变;视图中删除元素,原 Map 也变
  • 注意:视图不支持 add 操作,会抛出 UnsupportedOperationException

易错点4:泛型遗漏警告

//  Raw type 警告
for (Map.Entry entry : map.entrySet()) { ... }

//  加上泛型
for (Map.Entry<String, String> entry : map.entrySet()) { ... }

不加泛型虽然能编译通过,但 getKey()/getValue() 返回的是 Object,后续使用需要强转,既不安全也不优雅。

一张图记住三种遍历场景

场景推荐方式说明
只需要 keykeySet()无需多余对象
只需要 valuevalues()返回 Collection,非 Set
同时需要 key + valueentrySet()最佳实践,避免 N+1
遍历时需要删除元素Iterator.remove()唯一安全的删除方式
JDK8+ 函数式风格forEach((k,v)->{})内部已优化为 entrySet 遍历

底层原理速记(面试加分项)

Q: entrySet() 为什么能有迭代器效果?

  1. 增强 for → 编译器转为 Iterator
  2. entrySet() 返回内部类 EntrySet(继承 AbstractSet
  3. EntrySet.iterator() 返回 EntryIterator
  4. EntryIterator 继承 HashIterator,通过 “扫描桶数组 + 遍历链表/红黑树” 直接访问 Node 节点
  5. Node 本身实现了 Map.Entry 接口 → 零拷贝、惰性遍历

总结

维度keySetentrySet
返回类型Set<K>Set<Map.Entry<K,V>>
获取 value需额外 get()直接 getValue()
时间复杂度O(n × hash成本)O(n)
适用场景仅用 key同时用 key + value
本质Map 的 key 视图Map 的键值对视图

一句话口诀

单用 key 走 keySet,key-value 一起拿就走 entrySet;遍历删除用迭代器,entrySet 是视图不是副本。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值