算法—Java Map 核心方法与实战场景指南

目录

​编辑

一、频次统计与兜底神技(最常用)

[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 里提纯出来的数据(keySetvalues)。

  • 数据容器: 因为数据量是动态的,所以无脑选用 "弹簧数组" ArrayList 来接盘。

  • 桥梁:for 循环遍历,或者用构造函数,或者用 Stream 流,把数据从"来源"倒进"容器"里。

你现在的算法和数据结构基础已经串联得非常好了,不管是做算法题还是写真实的业务逻辑,这种数据转换的思维都会让你写代码时极其丝滑。

相关推荐
WWZZ20251 小时前
Sim2Sim理论与实践3:深度强化学习
人工智能·算法·机器人·深度强化学习·具身智能·四足·人形
会编程的土豆1 小时前
【数据结构与算法】栈的应用
数据结构·c++·算法
豆豆1 小时前
建站系统怎么选?2026年SaaS平台与开源CMS对比分析
java·开发语言·开源·cms·网站建设·网站制作·网站开发
神仙别闹1 小时前
基于C++实现的简单的SMTP服务器
服务器·开发语言·c++
菜菜的顾清寒1 小时前
力扣hot100(17) 缺失的第一个正数
算法·leetcode·职场和发展
鹓于1 小时前
PPT VBA随机选题系统实现详解
java·前端·javascript
光泽雨1 小时前
c#数值类型之间的自动转换
java·算法·c#
ZPC82102 小时前
moveit2 servo -movegroup aciton client arm_controller -rviz2
人工智能·算法·计算机视觉·机器人