news 2026/10/14 18:35:19

Map 的 putIfAbsent(k, v) 详解

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Map 的 putIfAbsent(k, v) 详解

承接前两篇早会分享:getOrDefault(借你用一下不存)→computeIfAbsent(没有就现场造一个,并真的存进去)。 这篇讲第三兄弟putIfAbsent:没有才放进去——而且放的是你提前准备好的值,不是函数现算的。

Map.putIfAbsent是JDK 8起 Map 接口新增的 default 方法(与 getOrDefault / computeIfAbsent 同批,来自JEP 126「Lambda 表达式与虚拟扩展方法」)。 但它的同名原子版本更早:ConcurrentMap.putIfAbsent在JDK 5(JSR 166)就有了。JDK 8 后接口默认实现非原子,ConcurrentHashMap仍覆盖为原子实现。

〇、三兄弟速查(核心对比表)

方法

判断"缺失"的依据

缺省值从哪来

写回 Map?

返回什么

getOrDefault(k, dv)

containsKey(k)

调用方预先算好的常量

否(只读)

有→旧值(哪怕值是 null);无→dv

putIfAbsent(k, v)

get(k) == null

调用方预先算好的值

是

无/值为 null→null;有值→旧值

computeIfAbsent(k, f)

get(k) == null

函数现算

是

有值→旧值;缺失→函数结果

三行里最容易看错的是第一列:getOrDefault用的判断是containsKey,另外两个用的是get(k) == null。 所以"key 存在、值为 null"这种脏数据,getOrDefault会原样返回 null,而putIfAbsent/computeIfAbsent会当成缺失、重新写入。

一、方法签名

  • key:要查找的键
  • value:key 缺失(或映射为 null)时,写进去的值
  • 返回值:V——旧值;如果原先没有映射(或映射为 null),返回null

对比:

方法

第二个参数是什么

getOrDefault(k, v)

一个值(兜底用,不写回)

putIfAbsent(k, v)

一个值(缺失时写回)

computeIfAbsent(k, f)

一个函数Function<K, V>(缺失时调用并写回)

一句话分水岭:putIfAbsent 传"值",computeIfAbsent 传"造值的函数"。

二、它到底做了什么

一句话:key 不存在(或 value 为 null)时,把 value 放进去并返回 null;否则什么都不做,返回原有的值。

JDK 默认实现(java.util.Map#putIfAbsent):

default V putIfAbsent(K key, V value) { V v = get(key); if (v == null) { v = put(key, value); // put 返回旧值,这里必然是 null } return v; }

注意这是get + put 两步,默认实现不保证原子性。ConcurrentHashMap覆盖为putVal(key, value, /*onlyIfAbsent*/ true)的 CAS 循环,才是真正原子的。

三个必须建立起来的认知:

  1. "懒"的是写,不是算—— value 参数在你调用它之前就已经算完了,省不了计算。
  2. 返回的是旧值,不是刚存进去的新值。
  3. 判断依据是get(key) == null,不是containsKey—— 所以"key 在、值为 null"会被覆盖。

三、与 getOrDefault / computeIfAbsent 的差异(重点)

差异 1:value 是"饿汉式",比 computeIfAbsent 白算一次

// ❌ 就算 key 已经有值,queryFromDB(id) 也照样执行 map.putIfAbsent(id, queryFromDB(id)); // ✅ 缺了才查 map.computeIfAbsent(id, this::queryFromDB);

这是 putIfAbsent 最容易被忽略的点:它省的是"写入",不省"计算"。computeIfAbsent收的是函数,函数体只有真缺时才执行。

差异 2:返回值语义不同(最容易踩)

Map<String, String> map = new HashMap<>(); String r = map.putIfAbsent("a", "v"); System.out.println(r); // null ← 注意! System.out.println(map.get("a")); // v ← 但 Map 里其实已经存进去了

返回 null 不代表没存进去。它返回的是"旧值",旧值本来就不存在,所以是 null。 正因为分不清"本来没映射"和"原来映射的是 null",javadoc 特意强调了这一点。

而computeIfAbsent返回的是"当前值",第一次调用就能直接拿到新值 —— 所以"取不到就建一个再用"的写法用它更顺:

// ✅ 取或建,一次拿到能用的对象 List<String> list = map.computeIfAbsent("a", k -> new ArrayList<>()); list.add("x"); // ❌ 用 putIfAbsent 就得再 get 一次 map.putIfAbsent("a", new ArrayList<>()); map.get("a").add("x");

差异 3:与 getOrDefault 相比

Map<String, String> map = new HashMap<>(); map.put("name", null); // key 存在,值是 null map.getOrDefault("name", "默认值"); // → null(containsKey 判定"有",返回旧值) map.putIfAbsent("name", "默认值"); // → 覆盖为 "默认值",返回 null(get==null 判定"缺")

一个只读、一个写回;一个把 null 当"有"、一个把 null 当"没有"。

差异 4:与手写if (!containsKey) put相比

if (!map.containsKey(k)) map.put(k, v); // key 在、值为 null → 保持 null 不动 map.putIfAbsent(k, v); // key 在、值为 null → 覆盖为 v

判断依据不同,行为就不同。语义上putIfAbsent更接近if (map.get(k) == null) map.put(k, v);。


四、必须记住的坑

坑 1:value 参数是饿汉式求值,昂贵计算/副作用会白跑

// ❌ 每次调用都会 new 一个 List,哪怕 key 已存在 map.putIfAbsent(k, new ArrayList<>()); // ❌ 每次都会打日志/发请求 map.putIfAbsent(k, remoteQuery(k));

想懒加载,就用 computeIfAbsent。

坑 2:返回的是旧值,别当成"存进去的值"

V cur = map.putIfAbsent(k, v); // cur 可能是 null,但 map 里已经有 v 了

需要"当前值"就自己再get一次,或者干脆用computeIfAbsent。

坑 3:默认实现不是原子的

Map<String, String> map = new HashMap<>(); // 非线程安全 // 并发下两个线程可能都 get 到 null,都执行 put —— 重复初始化 map.putIfAbsent(k, expensiveInit());

非线程安全的 Map 上并发调用putIfAbsent,并不能当锁用。 要原子性,容器必须是ConcurrentHashMap(它覆盖成了 CAS 实现)。

坑 4:禁 null 的 Map 上传 null value 会 NPE

ConcurrentHashMap/Hashtable/ConcurrentSkipListMap/Properties/Map.of(...)都不允许 value 为 null,putIfAbsent(k, null)直接抛NullPointerException。

坑 5:不可变 Map 上,缺失 key 会 UnsupportedOperationException

Map<String, String> m = Map.of("a", "b"); m.putIfAbsent("a", "x"); // ✅ key 已存在 → 只走 get,直接返回 "b",不抛 m.putIfAbsent("c", "x"); // ❌ key 缺失 → 走 put → UnsupportedOperationException

只读场景老老实实用getOrDefault。

坑 6:存进去的是引用,改它会污染 Map

map.putIfAbsent("a", new ArrayList<>()); map.get("a").add("x"); // 原 Map 里的 List 也被改了

想独立使用,先拷贝一份。这一点和computeIfAbsent完全一样。


五、null 相关行为速查(对齐前两篇)

场景

putIfAbsent 表现

value传 null(HashMap / LinkedHashMap / TreeMap)

✅ 合法:缺失则写入 null,返回 null

value传 null(CHM / Hashtable / Map.of)

❌ NullPointerException(容器本身禁 null value)

key传 null(HashMap / LinkedHashMap)

✅ 支持

key传 null(TreeMap / Hashtable / CHM / Map.of)

❌ NullPointerException

key 已存在且 value 非 null

不写入,返回旧值

key 已存在但 value 为 null

视为缺失,覆盖并返回 null

不可变 Map 上 key 缺失

❌ UnsupportedOperationException

想在"禁 null"的容器里表达空值,思路同前一篇:Optional包装,或空对象模式(Null Object Pattern)。

六、怎么选(扩展前两篇的选型表)

场景

推荐

只是取个值,默认结果是便宜常量(0、""、false)

✅getOrDefault

需要把值写回,且值算起来便宜(常量、已有对象)

✅putIfAbsent

默认值需要昂贵计算(查库、new 大对象)

✅computeIfAbsent

需要链式追加(如Map<String, List>分组)

✅computeIfAbsent

在ConcurrentHashMap上做原子"没有才放"

✅putIfAbsent(值现成)/computeIfAbsent(值要算)

存在就覆盖、不存在就插入

✅put(无条件覆盖)


七、典型真实用法

1. 幂等占位 / 分布式锁的味道(值已现成 + 要原子)

ConcurrentHashMap<String, String> lockMap = new ConcurrentHashMap<>(); // 只有第一个进来的线程拿到 null,可当作"抢到锁" if (lockMap.putIfAbsent(orderNo, "PROCESSING") == null) { try { // 只有抢到的线程执行处理 } finally { lockMap.remove(orderNo); } }

这里的关键前提是容器必须是 ConcurrentHashMap,靠的是它覆盖的原子实现。

2. 配置兜底:用户没配才用默认

Map<String, String> cfg = new HashMap<>(userConfig); cfg.putIfAbsent("timeout", "3000"); // 值便宜,写回一次即可

3. 想写回、但值算起来贵 → 换成 computeIfAbsent

// ❌ 饿汉式,白查库 map.putIfAbsent(id, userMapper.selectById(id)); // ✅ 懒加载 + 自动写回 map.computeIfAbsent(id, userMapper::selectById);

4. 列表分组(putIfAbsent 反而别扭)

Map<String, List<Order>> group = new HashMap<>(); // ✅ 首选 group.computeIfAbsent(order.getType(), k -> new ArrayList<>()).add(order); // ⚠️ 用 putIfAbsent 得两行,且 new ArrayList<>() 每次都会白建 group.putIfAbsent(order.getType(), new ArrayList<>()); group.get(order.getType()).add(order);

八、记忆口诀

putIfAbsent:查 key → 有值就返回旧值、不动它 → 没值(或值为 null)就把我准备好的值放进去,返回 null → 会改 Map → 但那个值我早就算好了,白算也得算。

前两篇的口诀一起串起来:

getOrDefault:查 key → 有就返回(哪怕值是 null)→ 没有才返回兜底值 → 不改 Map → 兜底值一定会被提前算出来。computeIfAbsent:查 key → 没有就现场算一个放进去并返回 → 有就直接给旧值 → 函数不动手就不执行。

三兄弟一句话区分:

方法

一句话

getOrDefault

借你用一下,不存

putIfAbsent

没有就放我准备好的值,存

computeIfAbsent

没有就现场造一个,存

选型的最短判断路径:

  1. 要往 Map 里写吗?—— 不写 →getOrDefault
  2. 要写,值昂贵 / 要链式追加吗?—— 是 →computeIfAbsent
  3. 要写,值现成 / 要原子占位吗?—— 是 →putIfAbsent

附:三兄弟源码级对照(JDK 默认实现)

// 1. getOrDefault —— 只读,判断用 containsKey default V getOrDefault(Object key, V defaultValue) { V v; return (((v = get(key)) != null) || containsKey(key)) ? v : defaultValue; } // 2. putIfAbsent —— 写回,判断用 get(key) == null,非原子 default V putIfAbsent(K key, V value) { V v = get(key); if (v == null) { v = put(key, value); } return v; } // 3. computeIfAbsent —— 写回 + 懒加载,判断用 get(key) == null default V computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction) { Objects.requireNonNull(mappingFunction); // 函数传 null → 立即 NPE V v; if ((v = get(key)) == null) { V newValue; if ((newValue = mappingFunction.apply(key)) != null) { // 函数返回 null → 不写回 put(key, newValue); return newValue; } } return v; }

注意getOrDefault的实际实现用(v = get(key)) != null || containsKey(key):先看值非空就直接返回,值非空但 key 不存在是不可能的,所以等价于"值非空或key 存在"——也就是把"key 存在、值为 null"判成"有"。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/14 18:33:50

死锁的四个必要条件:从多线程事故到系统化排查

1. 死锁不是玄学&#xff1a;从一次真实的多线程事故说起三年前我在某团队维护一个订单处理服务&#xff0c;有一阵子线上频繁报警&#xff0c;线程池被占满&#xff0c;请求大量超时。翻日志发现一段很诡异的循环&#xff1a;A任务在等B任务释放锁&#xff0c;B任务又在等A任务…

作者头像 李华
网站建设 2026/10/14 18:33:29

2026年全球AI大模型API聚合平台深度评测:企业级与开发者各自的首选

选大模型 API 聚合平台&#xff0c;2026 年公认的四个硬维度是&#xff1a;延迟、价格、模型覆盖、合规支付。本篇基于这四个维度对全球主流聚合服务做一轮深度评测&#xff0c;覆盖 OpenRouter、硅基流动、词元之河(TokenRiver.ai)等代表选手&#xff0c;并给出企业与开发者各…

作者头像 李华
网站建设 2026/10/14 18:31:52

二叉排序树实战:单词检查(Ⅱ)从选型到性能优化

1. 从"单词检查"到二叉排序树&#xff1a;一个被低估的数据结构实战场景单词检查这件事&#xff0c;乍一听像是英语老师批改作文的活儿&#xff0c;但放到编程语境里&#xff0c;它其实是一个非常经典的数据结构应用题。所谓单词检查&#xff0c;核心需求无非是&…

作者头像 李华
网站建设 2026/10/14 18:30:46

ZAB协议深度解析:从原理到生产环境实战

分布式系统里提到一致性&#xff0c;很多人第一反应是 Paxos、Raft&#xff0c;但真正在大型中间件里扛过线上流量的&#xff0c;ZAB 绝对是绕不开的一个。Zookeeper 作为分布式协调的基石组件&#xff0c;它的核心——ZAB 协议&#xff08;Zookeeper Atomic Broadcast&#xf…

作者头像 李华