资讯动态

Map 的 putIfAbsent(k, v) 详解

发布时间:2026/10/14 18:35:28 来源:尧图企业网站定制
承接前两篇早会分享getOrDefault借你用一下不存→computeIfAbsent没有就现场造一个并真的存进去。 这篇讲第三兄弟putIfAbsent没有才放进去——而且放的是你提前准备好的值不是函数现算的。Map.putIfAbsent是JDK 8起 Map 接口新增的 default 方法与 getOrDefault / computeIfAbsent 同批来自JEP 126「Lambda 表达式与虚拟扩展方法」。 但它的同名原子版本更早ConcurrentMap.putIfAbsent在JDK 5JSR 166就有了。JDK 8 后接口默认实现非原子ConcurrentHashMap仍覆盖为原子实现。〇、三兄弟速查核心对比表方法判断缺失的依据缺省值从哪来写回 Map返回什么getOrDefault(k, dv)containsKey(k)调用方预先算好的常量否只读有→旧值哪怕值是 null无→dvputIfAbsent(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要查找的键valuekey 缺失或映射为 null时写进去的值返回值V——旧值如果原先没有映射或映射为 null返回null对比方法第二个参数是什么getOrDefault(k, v)一个值兜底用不写回putIfAbsent(k, v)一个值缺失时写回computeIfAbsent(k, f)一个函数FunctionK, V缺失时调用并写回一句话分水岭putIfAbsent 传值computeIfAbsent 传造值的函数。二、它到底做了什么一句话key 不存在或 value 为 null时把 value 放进去并返回 null否则什么都不做返回原有的值。JDK 默认实现java.util.Map#putIfAbsentdefault 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 的差异重点差异 1value 是饿汉式比 computeIfAbsent 白算一次// ❌ 就算 key 已经有值queryFromDB(id) 也照样执行 map.putIfAbsent(id, queryFromDB(id)); // ✅ 缺了才查 map.computeIfAbsent(id, this::queryFromDB);这是 putIfAbsent 最容易被忽略的点它省的是写入不省计算。computeIfAbsent收的是函数函数体只有真缺时才执行。差异 2返回值语义不同最容易踩MapString, 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。 正因为分不清本来没映射和原来映射的是 nulljavadoc 特意强调了这一点。而computeIfAbsent返回的是当前值第一次调用就能直接拿到新值 —— 所以取不到就建一个再用的写法用它更顺// ✅ 取或建一次拿到能用的对象 ListString list map.computeIfAbsent(a, k - new ArrayList()); list.add(x); // ❌ 用 putIfAbsent 就得再 get 一次 map.putIfAbsent(a, new ArrayList()); map.get(a).add(x);差异 3与 getOrDefault 相比MapString, String map new HashMap(); map.put(name, null); // key 存在值是 null map.getOrDefault(name, 默认值); // → nullcontainsKey 判定有返回旧值 map.putIfAbsent(name, 默认值); // → 覆盖为 默认值返回 nullgetnull 判定缺一个只读、一个写回一个把 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);。四、必须记住的坑坑 1value 参数是饿汉式求值昂贵计算/副作用会白跑// ❌ 每次调用都会 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默认实现不是原子的MapString, String map new HashMap(); // 非线程安全 // 并发下两个线程可能都 get 到 null都执行 put —— 重复初始化 map.putIfAbsent(k, expensiveInit());非线程安全的 Map 上并发调用putIfAbsent并不能当锁用。 要原子性容器必须是ConcurrentHashMap它覆盖成了 CAS 实现。坑 4禁 null 的 Map 上传 null value 会 NPEConcurrentHashMap/Hashtable/ConcurrentSkipListMap/Properties/Map.of(...)都不允许 value 为 nullputIfAbsent(k, null)直接抛NullPointerException。坑 5不可变 Map 上缺失 key 会 UnsupportedOperationExceptionMapString, String m Map.of(a, b); m.putIfAbsent(a, x); // ✅ key 已存在 → 只走 get直接返回 b不抛 m.putIfAbsent(c, x); // ❌ key 缺失 → 走 put → UnsupportedOperationException只读场景老老实实用getOrDefault。坑 6存进去的是引用改它会污染 Mapmap.putIfAbsent(a, new ArrayList()); map.get(a).add(x); // 原 Map 里的 List 也被改了想独立使用先拷贝一份。这一点和computeIfAbsent完全一样。五、null 相关行为速查对齐前两篇场景putIfAbsent 表现value传 nullHashMap / LinkedHashMap / TreeMap✅ 合法缺失则写入 null返回 nullvalue传 nullCHM / Hashtable / Map.of❌ NullPointerException容器本身禁 null valuekey传 nullHashMap / LinkedHashMap✅ 支持key传 nullTreeMap / Hashtable / CHM / Map.of❌ NullPointerExceptionkey 已存在且 value 非 null不写入返回旧值key 已存在但 value 为 null视为缺失覆盖并返回 null不可变 Map 上 key 缺失❌ UnsupportedOperationException想在禁 null的容器里表达空值思路同前一篇Optional包装或空对象模式Null Object Pattern。六、怎么选扩展前两篇的选型表场景推荐只是取个值默认结果是便宜常量0、、false✅getOrDefault需要把值写回且值算起来便宜常量、已有对象✅putIfAbsent默认值需要昂贵计算查库、new 大对象✅computeIfAbsent需要链式追加如MapString, List分组✅computeIfAbsent在ConcurrentHashMap上做原子没有才放✅putIfAbsent值现成/computeIfAbsent值要算存在就覆盖、不存在就插入✅put无条件覆盖七、典型真实用法1. 幂等占位 / 分布式锁的味道值已现成 要原子ConcurrentHashMapString, String lockMap new ConcurrentHashMap(); // 只有第一个进来的线程拿到 null可当作抢到锁 if (lockMap.putIfAbsent(orderNo, PROCESSING) null) { try { // 只有抢到的线程执行处理 } finally { lockMap.remove(orderNo); } }这里的关键前提是容器必须是 ConcurrentHashMap靠的是它覆盖的原子实现。2. 配置兜底用户没配才用默认MapString, String cfg new HashMap(userConfig); cfg.putIfAbsent(timeout, 3000); // 值便宜写回一次即可3. 想写回、但值算起来贵 → 换成 computeIfAbsent// ❌ 饿汉式白查库 map.putIfAbsent(id, userMapper.selectById(id)); // ✅ 懒加载 自动写回 map.computeIfAbsent(id, userMapper::selectById);4. 列表分组putIfAbsent 反而别扭MapString, ListOrder 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判成有。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑