用computeIfAbsent()写记忆化递归,是一个看起来很优雅、网上教程也经常推荐的写法——这篇真实跑一遍才发现,这个"优雅写法"真实必然崩溃,而且不是偶尔,是每次都会。
上一篇:数组直接打印真实陷阱
用computeIfAbsent()实现斐波那契记忆化——这个写法在很多"函数式编程技巧"文章里被当作范例推荐。
MapComputeIfAbsentRecursiveDemo.java static int fibonacci(Map<Integer, Integer> memo, int n) { return memo.computeIfAbsent(n, k -> fibonacci(memo, k - 1) + fibonacci(memo, k - 2)); }
真实捕获 ConcurrentModificationException: null
真实结果证明这个"优雅"的递归写法在 HashMap 上真实必然崩溃(注意异常消息真实是 null——ConcurrentModificationException 的默认构造器不带任何描述文字,单纯依靠异常类型本身传达信息)。根本原因是 computeIfAbsent() 的完整生命周期是:检测key不存在 → 调用 mappingFunction 计算值 → 把结果真实写入map。在这个 mappingFunction(也就是这里的lambda)执行期间,HashMap 真实处于一种"正在进行结构性修改"的中间状态,如果这个回调内部又真实触发了对同一个map的另一次结构性修改(这里是递归调用真实再次触发另一层 computeIfAbsent),HashMap 内部的 modCount 检测机制会真实识别出这个冲突并立刻抛出异常。
不需要递归这么复杂——只要在computeIfAbsent的回调里,直接对同一个map做一次put(),会怎样?
MapComputeIfAbsentRecursiveDemo.java simpleMap.computeIfAbsent("b", k -> { simpleMap.put("c", 999); // 在computeIfAbsent回调内部直接修改同一个map return 2; });
直接修改同一个map真实抛出: ConcurrentModificationException
既然computeIfAbsent不能用于这种递归场景,正确的记忆化模式应该怎么写?
MapComputeIfAbsentRecursiveDemo.java static long fibCorrect(int n, Map<Integer, Long> memo) { if (n <= 1) return n; if (memo.containsKey(n)) return memo.get(n); long result = fibCorrect(n - 1, memo) + fibCorrect(n - 2, memo); // 递归先完成 memo.put(n, result); // 递归返回之后才真实修改map return result; }
正确写法计算fib(20) 真实结果: 6765 memo真实缓存了多少个中间结果: 19
真实结果证明了正确的模式:用传统的 containsKey() + get() + put() 三步分开操作,put() 真实只在递归调用完全返回之后才执行,不会出现"正在修改map的过程中又嵌套修改同一个map"的冲突场景。computeIfAbsent() 真正适合的场景是"计算过程本身不涉及对同一个map的递归访问"——比如系列前面常见的 map.computeIfAbsent(key, k -> new ArrayList<>()) 这种简单的默认值初始化。
用最小能复现问题的 fibonacci(memo, 3) 逐步拆解:每一步的 modCount 和 memo 内容,都是真实运行这段代码时的状态。