承接前两篇早会分享:getOrDefault(借你用一下不存)→computeIfAbsent(没有就现场造一个,并真的存进去)。 这篇讲第三兄弟putIfAbsent:没有才放进去——而且放的是你提前准备好的值,不是函数现算的。
Map.putIfAbsent是JDK 8起 Map 接口新增的 default 方法(与 getOrDefault / computeIfAbsent 同批,来自JEP 126「Lambda 表达式与虚拟扩展方法」)。 但它的同名原子版本更早:ConcurrentMap.putIfAbsent在JDK 5(JSR 166)就有了。JDK 8 后接口默认实现非原子,ConcurrentHashMap仍覆盖为原子实现。
〇、三兄弟速查(核心对比表)
方法 | 判断"缺失"的依据 | 缺省值从哪来 | 写回 Map? | 返回什么 |
|
| 调用方预先算好的常量 | 否(只读) | 有→旧值(哪怕值是 null);无→dv |
|
| 调用方预先算好的值 | 是 | 无/值为 null→null;有值→旧值 |
|
| 函数现算 | 是 | 有值→旧值;缺失→函数结果 |
三行里最容易看错的是第一列:getOrDefault用的判断是containsKey,另外两个用的是get(k) == null。 所以"key 存在、值为 null"这种脏数据,getOrDefault会原样返回 null,而putIfAbsent/computeIfAbsent会当成缺失、重新写入。
一、方法签名
- key:要查找的键
- value:key 缺失(或映射为 null)时,写进去的值
- 返回值:
V——旧值;如果原先没有映射(或映射为 null),返回null
对比:
方法 | 第二个参数是什么 |
| 一个值(兜底用,不写回) |
| 一个值(缺失时写回) |
| 一个函数 |
一句话分水岭: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 循环,才是真正原子的。
三个必须建立起来的认知:
- "懒"的是写,不是算—— value 参数在你调用它之前就已经算完了,省不了计算。
- 返回的是旧值,不是刚存进去的新值。
- 判断依据是
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 表现 |
| ✅ 合法:缺失则写入 null,返回 null |
| ❌ NullPointerException(容器本身禁 null value) |
| ✅ 支持 |
| ❌ NullPointerException |
key 已存在且 value 非 null | 不写入,返回旧值 |
key 已存在但 value 为 null | 视为缺失,覆盖并返回 null |
不可变 Map 上 key 缺失 | ❌ UnsupportedOperationException |
想在"禁 null"的容器里表达空值,思路同前一篇:Optional包装,或空对象模式(Null Object Pattern)。
六、怎么选(扩展前两篇的选型表)
场景 | 推荐 |
只是取个值,默认结果是便宜常量(0、""、false) | ✅ |
需要把值写回,且值算起来便宜(常量、已有对象) | ✅ |
默认值需要昂贵计算(查库、new 大对象) | ✅ |
需要链式追加(如 | ✅ |
在 | ✅ |
存在就覆盖、不存在就插入 | ✅ |
七、典型真实用法
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 | 没有就现场造一个,存 |
选型的最短判断路径:
- 要往 Map 里写吗?—— 不写 →
getOrDefault - 要写,值昂贵 / 要链式追加吗?—— 是 →
computeIfAbsent - 要写,值现成 / 要原子占位吗?—— 是 →
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"判成"有"。