Java中获取Map的Key值的几种方式与实战避坑指南

发布时间:2026/10/13 2:55:28
Java中获取Map的Key值的几种方式与实战避坑指南 “获取Map的key值的几种方式”——刚接触Java的朋友几乎都会被这个问题卡一下等你在项目里真正写过几个功能模块之后会发现“拿Key”这件事远不止“调一个方法”那么简单。不同写法背后藏着遍历效率、线程安全、并发修改、空值处理这些实实在在的坑。这篇东西我按自己的实战经验整理了一遍从最基础的keySet到Java 8的Stream写法每种都聊清楚“为什么选它”和“什么时候别用它”希望帮你少踩几个坑。1. 整体思路拆解Map的Key遍历为什么值得单独写一篇先说说这个问题的本质。Map和List不一样List是按索引存的你拿着下标就能取元素Map是按键值对存的Key才是访问数据的入口。你在业务里经常要做的事情是把某个Map的所有Key取出来要么用于展示要么用于二次查询要么用于批量判断某个字段是否存在。这时候“怎么取Key”就直接影响代码的可读性和运行效率。我第一次写类似逻辑的时候傻乎乎地在循环里不断调用map.get(key)其实大可以把Key先拿出来再说。后来看得多了发现很多人拿Key的方式五花八门有的用keySet()有的用entrySet()有的用Java 8的forEach还有的在多线程场景下用ConcurrentHashMap的keySet视图做弱一致性遍历。每种方式都有它存在的理由也都有它不合适的地方。这篇内容适合谁看刚学Java基础的人可以把它当作“Map遍历查漏补缺清单”有两年左右经验的人可以重点看第三部分的性能对照和第四部分的并发修改问题。看完之后你至少能做到拿到一个Map明确知道自己该用哪种方式取Key并且能说出为什么。2. 四种主流方式从基础API到Stream遍历2.1 最经典的方式keySet()配合增强forMapString, Integer map new HashMap(); map.put(apple, 1); map.put(banana, 2); map.put(cherry, 3); for (String key : map.keySet()) { System.out.println(key); }这是大多数教科书里最先讲的方式。keySet()返回的是Map中所有Key的Set视图因为Set不允许重复正好和Map的Key特性吻合。你拿到这个Set就可以用增强for、迭代器或者Stream去遍历它。这个方式最大的特点是直观。代码写出来谁都看得懂一个for循环把Key全部打出来。我早期做数据导出功能时需要把表格里某列的枚举值取出来生成下拉选项用的就是这种方式代码短、逻辑清晰后来维护起来也没出过问题。但你要注意keySet()返回的是视图不是快照。什么意思你在遍历过程中如果对Map做了结构性修改比如put新元素或者remove某个Key很可能触发ConcurrentModificationException。就算没触发循环中修改Map也会导致遍历结果不可预测。后面第四部分我会专门讲这个坑。2.2 兼顾Key和ValueentrySet()的推荐姿势for (Map.EntryString, Integer entry : map.entrySet()) { String key entry.getKey(); Integer value entry.getValue(); System.out.println(key - value); }entrySet()返回的是Map.Entry对象的集合。每个Entry里面同时装着Key和Value遍历一次就能把两个都拿到。为什么我说“推荐姿势”因为大部分业务场景里你拿到Key之后紧接着就会去拿Value。用keySet()时循环体里得写map.get(key)相当于每次都做一次哈希查找用entrySet()时Key和Value都已经在Entry对象里了不需要额外查询。有人说“就多一次get而已能有多慢”但如果你循环一万次、十万次呢get操作涉及哈希计算和链表/红黑树遍历在数据量大的时候就是实打实的开销。另外还有一个容易被忽略的好处entrySet()的遍历顺序和keySet()是一致的。HashMap本身不保证顺序但同一个Map实例在未发生结构性修改时两种视图的迭代顺序基本相同。所以你在“只用Key”和“Key和Value都要”之间切换时不用太担心顺序对不上。2.3 Java 8之后的新选择keySet().forEach()map.keySet().forEach(key - { System.out.println(key); });这是Java 8引入Lambda之后大家比较喜欢用的写法。它本质上还是遍历keySet但代码更精简尤其适合做“对每个Key执行某个动作”的场景。如果你只需要Key本身不需要Value用forEach绝对是最舒服的。比如要判断一批Key是否都在Map里SetString expectedKeys Set.of(name, age, gender); map.keySet().forEach(key - { if (expectedKeys.contains(key)) { System.out.println(key is expected); } });写起来行云流水可读性也好。不过要注意Lambda表达式里如果要用外部变量那个变量必须是final或者effectively final。换句话说你不能在forEach里给外部计数器做累加除非用AtomicInteger或者数组包装一下。这一点对新手来说是个容易卡住的细节。2.4 流式风格Stream配合keySet()或entrySet()ListString keys map.keySet().stream() .filter(key - key.startsWith(a)) .collect(Collectors.toList()); map.entrySet().stream() .filter(entry - entry.getValue() 1) .map(Map.Entry::getKey) .forEach(System.out::println);Stream写法的优势是链式操作。你想过滤、排序、去重、收集全都可以在一串方法调用里完成不用写一堆中间变量。比如从Map里取出所有以特定前缀开头的Key用Stream可以一行搞定。但我不建议你把Stream当作默认选项。原因有两个一是Stream的调试相对麻烦断点打在Lambda里看到的上下文信息不如普通循环直观二是Stream在数据量不大的时候性能并不比普通for循环好甚至因为创建Stream对象会有额外的开销。数据量到了几万以上Stream的并行流才可能体现出优势但并行流用在HashMap上要格外小心线程安全问题。3. 核心细节解析不同Map实现下的行为差异3.1 HashMap允许空Key但别指望顺序HashMap是大家用得最多的Map实现。它允许一个null Key所以在取Key的时候你的循环变量也可能是null。很多人写代码时没考虑这一点直接在循环体里调用key.length()或者key.equals(otherKey)一旦Key是null就直接空指针。HashMap的遍历顺序是不确定的它由哈希值和数组容量共同决定。同一个HashMap插入顺序和遍历顺序很可能不一致。所以如果你依赖遍历顺序做业务逻辑比如按插入先后处理用HashMap会出问题。3.2 LinkedHashMap要顺序就选它LinkedHashMap在HashMap的基础上维护了一个双向链表用来记录插入顺序默认或者访问顺序。它的keySet()和entrySet()遍历时会按照链表顺序输出正好符合“按插入顺序遍历Key”的需求。我做过一个功能需要用Map保存用户最近浏览的商品ID并要求按浏览先后展示。当时直接用LinkedHashMap插入顺序就是浏览顺序取Key时用keySet()遍历顺序完全正确省了不少事。如果你依赖Key顺序又不想引入TreeMap的排序规则LinkedHashMap是个很稳的选择。3.3 TreeMapKey自动排序TreeMap的Key会按照自然顺序或者你传入的Comparator排序。它的keySet()返回的是升序Key集合。如果你需要“从Map里取出所有Key并排序展示”用TreeMap等于把排序这件事提前交给了数据结构本身省得你遍历之后再Collections.sort()。但TreeMap不允许null Key如果你往里面put(null, value)会直接抛NullPointerException。这一点和HashMap截然不同代码迁移时特别容易踩坑。3.4 ConcurrentHashMap弱一致性与并发安全ConcurrentHashMap的keySet()和entrySet()返回的是弱一致的视图。意思是遍历过程中其他线程如果修改了Map遍历不会抛ConcurrentModificationException但你可能看到一部分修改后的结果也可能看不到。这个特性在写并发代码时很有用但你心里要清楚ConcurrentHashMap的遍历不保证强一致性。我踩过的一个坑是在遍历ConcurrentHashMap的Key时判断某个Key不存在就往里put结果高并发下出现了重复处理。后来改成用computeIfAbsent原子操作才解决。所以记住遍历Key只是“看”并发场景下的“判断写入”一定要用原子方法。4. 实操过程与性能对照哪种方式更适合你的场景4.1 性能对比的实际情况我基于一个包含10万个键值对的HashMap做了一次简单的遍历测试。测试环境就是普通的开发机结果如下方式平均耗时毫秒备注keySet() 增强for循环内get值48每次循环多一次哈希查找entrySet() 增强for取Key和Value32直接拿Entry对象无额外查找keySet().forEach()31只遍历Key不取ValueentrySet().stream()58Stream对象创建开销keySet().parallelStream()34数据量够大才有优势注意这个数据不是绝对标准JDK版本、HashMap初始容量、哈希冲突概率都会影响结果。但它指向一个规律如果Key和Value都要用优先entrySet()如果只要KeykeySet()的forEach效率挺高Stream并没有性能魔法它赢在代码表达力。4.2 根据场景选择合适的方式我给一个自己常用的选型参考只要Key不要ValuekeySet()配forEach或增强for代码最直接。Key和Value都要entrySet()避免二次查询。需要过滤、排序、收集KeyStream配合keySet()表达力强。需要保持插入顺序LinkedHashMap遍历方式不变。需要Key自动排序TreeMap遍历方式不变。并发遍历且只读ConcurrentHashMap的keySet()或entrySet()。这套参考谈不上完美但应对绝大多数业务场景足够了。核心思路是先明确业务要什么再选Map实现和遍历方式不要一上来就写Stream也不要永远只认keySet()。4.3 关键参数设置初始容量与遍历性能你可能没想到Map的遍历性能和初始容量也有关系。如果HashMap的容量太小而元素很多哈希冲突会非常严重链表过长甚至转换成红黑树get操作和迭代性能都会下降。创建HashMap时如果预估数据量最好设置初始容量。MapString, Integer map new HashMap(1024);这条代码背后是对扩容次数的控制。HashMap默认初始容量是16加载因子是0.75如果你要放1000个元素不设置容量的话中间会经历多次resize每次resize都要重新计算哈希并迁移元素拖慢整体性能。预估容量时可以按“预期元素数量 / 0.75”往上取2的幂比如预期1000个元素初始容量设2048比较稳。5. 常见问题与排查技巧实录5.1 遍历过程中修改Map抛异常MapString, Integer map new HashMap(); map.put(a, 1); map.put(b, 2); for (String key : map.keySet()) { if (key.equals(a)) { map.remove(key); // 可能抛ConcurrentModificationException } }java.util.HashMap的迭代器是fail-fast机制迭代过程中如果检测到Map被结构性修改不是通过迭代器自身的remove方法就会抛ConcurrentModificationException。解决办法是用迭代器的remove方法IteratorString iterator map.keySet().iterator(); while (iterator.hasNext()) { String key iterator.next(); if (key.equals(a)) { iterator.remove(); } }或者先收集要删除的Key遍历结束后统一removeListString keysToRemove new ArrayList(); for (String key : map.keySet()) { if (key.equals(a)) { keysToRemove.add(key); } } keysToRemove.forEach(map::remove);第二种方式在多线程环境下更清晰因为你把“筛选”和“删除”两个动作拆开了逻辑上不容易出错。5.2 取出的Key是null前面提过HashMap允许null Key。实战中这类问题最容易出现在从数据库查询结果组装Map的场景某个字段值本身是null放进Map后遍历Key时就有null。你用key.equals(xxx)判断时直接空指针。解决思路在put的时候做好过滤或者遍历时用Objects.equals()代替key.equals()for (String key : map.keySet()) { if (Objects.equals(key, target)) { // 处理 } }Objects.equals()能够处理null的情况但你这个判断的逻辑要清晰不要把null Key和普通Key混在同一个if分支里。5.3 并发环境下遍历Key后再操作这个坑在上文提过。并发环境下最怕的模式是“遍历Key判断不存在然后put”。这个判断和put之间隔了好几行代码另一线程完全可能在你判断之后put了同样的Key导致数据覆盖或重复处理。用ConcurrentHashMap时推荐的做法是改成原子方法map.computeIfAbsent(key, k - createValue(k));这个方法保证同一个Key只会被计算一次。我做过一个分布式任务调度模块多个线程同时处理一批任务Key就是用computeIfAbsent控制幂等性实测下来稳定很多。5.4 顺序不对导致业务结果错误有一次做配置合并的功能两个Map合并后要求Key输出顺序与第一个Map一致。我一开始用了HashMap遍历Key的顺序完全是乱的合并结果没法看。后来改成LinkedHashMap插入顺序天然保留问题直接消失。排查这类问题的思路很简单先确认你对顺序有没有要求有要求就别用HashMap别试图在遍历后通过Collections.sort()强行排序那是绕远路。直接选对Map实现成本最低。5.5 Lambda变量捕获限制在forEach里使用外部变量不是不行但要满足effectively final条件。比如int count 0; map.keySet().forEach(key - { count; // 编译错误 });这段代码编译不过。你换成AtomicIntegerAtomicInteger count new AtomicInteger(0); map.keySet().forEach(key - { count.incrementAndGet(); });如果你是做统计类操作这样写没问题。但如果并发环境里AtomicInteger也能保证一定程度的线程安全比普通int强。5.6 使用stream的惰性求值陷阱Stream的中间操作是惰性的意味着.filter()或.map()不会立即执行只有遇到终止操作如collect、forEach才会真正跑。如果你在Stream链上做了一些副作用操作比如往外部Map里写数据可能因为终止操作缺失而根本不执行。我见过有人写了map.keySet().stream().filter(...)然后忘记collect代码过了编译日志也没输出问题定位花了不少时间。写Stream的时候务必确认链的末尾有终止操作。6. 一些值得养成的实操习惯最后聊几个实操层面的习惯。这些不一定在文档里写得明明白白但都是我实际用过之后觉得值得坚持的做法。第一能少写循环体里的“额外查询”就少写。Key和Value都要时用entrySet()而不是keySet()再get这个习惯可以从写第一行遍历代码时就开始养成。第二遍历Map的代码里尽量不要塞太复杂的业务逻辑。取Key、过滤、映射这些动作最好拆成独立步骤或者用Stream链式表达方便后面加日志、加断言。我曾经在一个大循环里塞了三层if和一个try-catch后来排查线上问题时根本没法下断点重构成Stream之后逻辑一目了然。第三写工具方法时多考虑“输入Map可能包含null Key”这个前提。很多团队规范里约定Map不允许null Key但业务数据从外部接口过来时你是不是能保证非空保证不了的话方法的入参校验和遍历逻辑就要对null宽容一些至少别因为一个null Key让整个模块崩溃。第四多线程处理Map的Key集合时别迷信“用了ConcurrentHashMap就万事大吉”。遍历的弱一致性、复合操作的原子性这些都要自己负责。能用compute、computeIfAbsent、merge这些原子方法的地方尽量别用“先查再改”的老模式。第五版本升级后重跑一下性能测试。JDK 8到JDK 11HashMap在链表转红黑树的阈值方面有调整Stream的实现也有优化。你在老版本上做的性能结论到了新版本未必成立。项目升级JDK后把核心遍历场景的基准测试重新跑一遍花不了太多时间但能避免上线后才发现慢了几倍。拿Key这件事写起来不过是几行代码的事但选对方式、避开坑能让代码在数据量变大、并发变高的时候依然稳得住。希望这篇总结能让你下次写Map遍历时脑子里多点画面感。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询