目录
[1. getOrDefault(Object key, V defaultValue)](#1. getOrDefault(Object key, V defaultValue))
[2. putIfAbsent(K key, V value)](#2. putIfAbsent(K key, V value))
[二、核心状态查询(O(1) 极速定位)](#二、核心状态查询(O(1) 极速定位))
[1. containsKey(Object key)](#1. containsKey(Object key))
[2. size()](#2. size())
[1. put(K key, V value)](#1. put(K key, V value))
[2. get(Object key)](#2. get(Object key))
[3. remove(Object key)](#3. remove(Object key))
[1. keySet()](#1. keySet())
[2. values()](#2. values())
[3. entrySet()](#3. entrySet())
[1. keySet():只关心"谁来了",不在乎"他干了啥"](#1. keySet():只关心“谁来了”,不在乎“他干了啥”)
[2. values():只关心"具体数字",不在乎"它是谁的"](#2. values():只关心“具体数字”,不在乎“它是谁的”)
[3. entrySet():又要"人",又要"数据"(性能之王)](#3. entrySet():又要“人”,又要“数据”(性能之王))
[⚠️ 为什么不推荐用 keySet 来替代?](#⚠️ 为什么不推荐用 keySet 来替代?)
[1. 究极偷懒法:利用构造函数(一行代码搞定)](#1. 究极偷懒法:利用构造函数(一行代码搞定))
[2. 高级过滤法:Java 8 Stream 流(后端必考神技)](#2. 高级过滤法:Java 8 Stream 流(后端必考神技))
在刷算法题(尤其是 LeetCode 上的哈希表、滑动窗口、双指针等题型)时,Map(通常指 HashMap)是出场率极高的数据结构。为了方便你在写算法或总结技术博客时查阅,我把算法中最常用、最核心的 Map 方法按实战场景进行了分类:
一、频次统计与兜底神技(最常用)
1. getOrDefault(Object key, V defaultValue)
-
作用: 获取 key 对应的值,如果 key 不存在,则返回给定的默认值。
-
算法场景: 绝对的频次统计标配! 比如你代码里的
map.put(num, map.getOrDefault(num, 0) + 1),一行代码搞定"有则加一,无则建一"。
2. putIfAbsent(K key, V value)
-
作用: 只有当 Map 中不存在这个 key 时,才把键值对存进去;如果已经有了,就什么都不做。
-
算法场景: 常用于记忆化搜索(Memoization)或缓存图的节点访问状态,防止重复计算。
二、核心状态查询(O(1) 极速定位)
1. containsKey(Object key)
-
作用: 判断 Map 中是否包含指定的 key。
-
算法场景: 经典神题 "两数之和 (Two Sum)" 的灵魂方法。用来极速判断我们需要的另一个目标数字是否已经遍历过了。
2. size()
-
作用: 返回 Map 中包含的键值对的数量(也就是有多少个不同的 key)。
-
算法场景: 就像你这道《水果成篮》一样,专门用来限制滑动窗口内"种类的最大/最小数量"。
三、基础的增删改查
1. put(K key, V value)
- 作用: 存入键值对。如果 key 已经存在,会覆盖原来的值。
2. get(Object key)
- 作用: 根据 key 取值。如果 key 不存在,会返回 null(注意: 在算法题中,如果直接对可能返回 null 的结果进行自动拆箱参与数学运算,会导致空指针异常,所以经常用
getOrDefault替代)。
3. remove(Object key)
-
作用: 把指定的 key 连同它的值一起从 Map 中彻底删掉。
-
算法场景: 滑动窗口左指针收缩时的必备动作。当某种元素的频次降为 0 时,必须 remove 掉,否则
size()不会减小。
四、遍历与结算(循环结束后的扫尾工作)
很多算法题在循环统计完之后,需要遍历 Map 来找出"出现次数最多"的元素,或者把结果提取出来。这时候就需要用到下面三个遍历视图:
1. keySet()
-
作用: 返回一个包含所有 key 的 Set 集合。
-
算法场景: 当你只关心有哪些 key 时使用。可以使用
for (int key : map.keySet())进行遍历。
2. values()
-
作用: 返回一个包含所有 value 的 Collection 集合。
-
算法场景: 当你只关心出现过哪些频次,完全不在乎是谁出现了这么多次时使用。比如求频次数组的最大值。
3. entrySet()
-
作用: 返回包含所有键值对(
Map.Entry对象)的 Set 集合。 -
算法场景: 遍历性能最高的方式! 当你需要同时用到 key 和 value 时(比如找出现次数大于 3 次的数字),务必用这个:
java
for (Map.Entry<Integer, Integer> entry : map.entrySet()) {
int key = entry.getKey();
int val = entry.getValue();
// do something...
}
提示: 在日常做算法题时,把这十个方法刻在 DNA 里,所有的哈希表相关题目基本上都能横着走了。
五、后端业务实战场景演练(以抽奖系统为例)
为了让你印象更深刻,我们结合你正在准备简历的在线抽奖系统这种真实的后端业务场景,来分别为这三个遍历方法举个最贴切的例子。
假设在我们的抽奖系统里,为了做数据统计,我们跑完一段逻辑后得到了下面这个 Map:
java
// 记录每个用户(Key:用户 ID)在今天一共抽中了多少次奖(Value:中奖次数)
Map<Integer, Integer> userWinCountMap = new HashMap<>();
userWinCountMap.put(1001, 5); // 用户 1001 中了 5 次
userWinCountMap.put(1002, 1); // 用户 1002 中了 1 次
userWinCountMap.put(1003, 3); // 用户 1003 中了 3 次
我们来看看在这份数据上,这三种遍历方式分别在什么场景下大显身手:
1. keySet():只关心"谁来了",不在乎"他干了啥"
-
业务场景: 运营部门说,只要今天中过奖的用户(不管中了多少次),都要统一给他们发一条"恭喜中奖"的短信通知。我们需要把所有中过奖的用户 ID 提取出来放入一个 List 交给短信服务。
-
代码实现:
java
List<Integer> luckyUsers = new ArrayList<>();
// 我们只关心 Key(用户 ID),完全不需要用到 Value(中奖次数)
for (Integer userId : userWinCountMap.keySet()) {
luckyUsers.add(userId);
}
// 最终 luckyUsers 里装的就是 [1001, 1002, 1003]
- 算法题对应: 比如你想统计图里有哪些独立的节点,或者单纯想把去重后的元素收集到一个数组里。
2. values():只关心"具体数字",不在乎"它是谁的"
-
业务场景: 财务部门说,想核对一下今天系统一共派发了多少个奖品出去。他们根本不关心这些奖品是发给了张三还是李四,只在乎所有"中奖次数"的总和。
-
代码实现:
java
int totalPrizesDistributed = 0;
// 我们只关心 Value(中奖次数),完全不需要知道 Key(用户 ID)是谁
for (Integer count : userWinCountMap.values()) {
totalPrizesDistributed += count;
}
// 最终 totalPrizesDistributed = 5 + 1 + 3 = 9
- 算法题对应: 比如你需要求频率最高的频次是多少(不用求具体是哪个数),或者把所有出现过的频次做个累加。
3. entrySet():又要"人",又要"数据"(性能之王)
-
业务场景: 抽奖活动结束了,我们要评选出今天的"锦鲤大奖"------找出今天中奖次数最多的那个用户,并且在首页展示他的 ID 和中奖次数。这就意味着,我们在比对数据的同时,绝不能丢失它对应的"主人"。
-
代码实现:
java
int maxWinCount = 0; // 记录最大的中奖次数
int luckyDogId = -1; // 记录对应锦鲤的用户 ID
// 我们既需要用 Value 去比大小,又需要把胜出的 Key 记录下来
for (Map.Entry<Integer, Integer> entry : userWinCountMap.entrySet()) {
int currentId = entry.getKey();
int currentCount = entry.getValue();
// 如果当前用户的中奖次数打破了记录
if (currentCount > maxWinCount) {
maxWinCount = currentCount; // 更新最大次数
luckyDogId = currentId; // 把皇冠戴给这个新用户
}
}
// 最终遍历结束,找到锦鲤:luckyDogId = 1001, maxWinCount = 5
⚠️ 为什么不推荐用 keySet 来替代?
很多新手会这么写,这是典型的大忌:
java
// 错误示范:性能极差
for (Integer currentId : userWinCountMap.keySet()) {
// 每次拿到 Key 后,还要再调用一遍 get 去 Map 里重新找一遍 Value
int currentCount = userWinCountMap.get(currentId);
// ... 后续比对逻辑
}
-
原因:
get(key)操作底层是要经过哈希计算和寻址的。你用keySet遍历,等于把所有的元素遍历了一遍,然后针对每个元素又去 Map 里重新搜了一遍。 -
而
entrySet()就像是连人带行李一起打包丢给你,直接调用entry.getValue()是 O(1) 的光速操作,不用再去底层重新找了。所以在算法题中,只要你需要同时用到 Key 和 Value,请无脑锁定entrySet()!
六、真实企业开发中更高级、更优雅的搭配手法
你这波"举一反三"非常精准!你的直觉完全正确。
在 Java 后端开发中,当我们面对"动态数据"(也就是在代码运行之前,你根本不知道会有多少条数据)时,List<T> list = new ArrayList<>(); 搭配 for 循环往里 add(),绝对是出场率高达 99% 的黄金组合!
既然你已经悟到了这一层,作为正在向 Java 后端工程师迈进的开发者,我必须再教你两招真实企业开发中更高级、更优雅的搭配手法。刚才那段代码虽然逻辑完美,但在大厂的真实代码库里,我们通常会用下面这两种方式来"秀操作":
1. 究极偷懒法:利用构造函数(一行代码搞定)
如果你只是单纯地想把 Map 里所有的 Key(或者任何一个 Set 集合)原封不动地转换成一个 ArrayList,你根本不需要写 for 循环!
ArrayList 的缔造者早就为你准备好了一个极其贴心的构造方法------允许你在 new 的时候,直接塞进去一个其他的集合。
// 一行代码,直接把 keySet 塞进 ArrayList 的肚子里
List<Integer> luckyUsers = new ArrayList<>(userWinCountMap.keySet());
- 底层原理: 这个写法不仅代码短,而且性能比 for 循环一个个 add() 还要好!因为它的底层会直接计算出 keySet 的大小,一次性把底层数组扩容到位,然后调用 C++ 级别的底层的
System.arraycopy瞬间把数据复制过去,极其暴躁且高效。
2. 高级过滤法:Java 8 Stream 流(后端必考神技)
假设现在你的抽奖系统业务逻辑变了:运营部门说,今天中奖次数大于等于 3 次的用户才算"超级锦鲤",我们只把这些超级锦鲤的 ID 提取出来存进 List。
如果用传统的 for 循环,你要写 if 判断。但在现代 Spring Boot 开发中,大家都在用 Stream 流,代码读起来就像用嘴说话一样流畅:
java
List<Integer> superLuckyUsers = userWinCountMap.entrySet().stream()
// 第一步:过滤 (filter),只保留中奖次数 >= 3 的键值对
.filter(entry -> entry.getValue() >= 3)
// 第二步:映射 (map),只要 Key(用户 ID)
.map(Map.Entry::getKey)
// 第三步:收集 (collect),把留下来的 Key 全部装进一个新的 List 里
.collect(Collectors.toList());
七、总结你的顿悟
你总结的搭配模式非常核心。在 Java 集合的宏观世界里,我们其实就是在玩一套"乐高积木":
-
数据来源: 可能是从数据库查出来的(原本就是 List),也可能是像咱们这样从 HashMap 里提纯出来的数据(
keySet、values)。 -
数据容器: 因为数据量是动态的,所以无脑选用 "弹簧数组"
ArrayList来接盘。 -
桥梁: 用
for循环遍历,或者用构造函数,或者用Stream流,把数据从"来源"倒进"容器"里。
你现在的算法和数据结构基础已经串联得非常好了,不管是做算法题还是写真实的业务逻辑,这种数据转换的思维都会让你写代码时极其丝滑。
