问题发现:

在写力扣的 70. 爬楼梯 时,写了如下代码:

class Solution {
    Map<Integer, Integer> map = new HashMap<>();

    public int climbStairs(int n) {
        if (n <= 1) {   // 边界条件:n == 1或者n == 0
            return 1;
        }
        int ans = 0;
        ans += map.getOrDefault(n - 1, climbStairs(n - 1));	// 
        ans += map.getOrDefault(n - 2, climbStairs(n - 2));

        map.put(n, ans);
        return ans;
    }
}
  • 代码使用 map.getOrDefault 函数,我是想完成如下操作:如果存在 key == n-1 or n-2,就直接将其 value 返回,而不进行递归
  • 但是提交之后一直超时

于是,将代码交给了通义千问,并将问题和原因总结如下:

  • 即使 n-1 已经在 map 中,climbStairs(n - 1) 仍然会被执行(因为 Java 采用 eager evaluation 策略)
  • 所以不能用 getOrDefault 来实现记忆化递归

eager evaluation 策略

关于 eager evaluation 策略,也是第一回看到这个词,于是让通义总结了一下:

Java 官方文档和语言规范(Java Language Specification, JLS)明确说明了 Java 采用 及早求值(eager evaluation) 策略,尤其是在方法调用时对参数的处理。

官方依据:Java 语言规范(JLS)

来源:The Java® Language Specification, Java SE 17 Edition(如下图)

Section 15.12.4. Run-Time Evaluation of Method Invocation 下的 15.12.4.2. Evaluate Arguments

翻译:

参数表达式(如果有的话)将按照从左到右的顺序依次求值。

这意味着:在真正进入方法体之前,所有传入的参数表达式都必须先被完全求值

举例说明

当写:

map.getOrDefault(key, expensiveComputation());

根据 JLS §15.12.4:

  1. JVM 首先计算 key 的值;
  2. 然后立即调用 expensiveComputation() 并等待其返回结果
  3. 最后才调用 getOrDefault 方法,把这两个已经求好值的结果作为参数传进去。

因此,无论 key 是否存在于 map 中,expensiveComputation() 一定会被执行

补充:对比延迟求值(lazy evaluation)

以 computeIfAbsent 为例,它的签名是:

V computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction)

注意:第二个参数是 一个函数对象(Function,而不是一个值。

  • 你传入的是一个 lambda 表达式,它会被包装成一个 Function 实例。
  • computeIfAbsent 内部决定是否调用 mappingFunction.apply(key)
  • 如果 key 已存在,它根本不会调用这个函数 —— 这就是延迟求值(lazy evaluation) 的体现。

getOrDefault 的签名是:

V getOrDefault(Object key, V defaultValue)

第二个参数是 值类型 V,所以调用前必须知道这个值是多少 → 必须提前计算。

关于延迟求值(lazy evaluation) ,可以参考这篇文章:Java的Stream API:惰性求值(Lazy Evaluation)与短路操作的性能优势

参考:

[1] 通义千问

[2] 15.12.4. Run-Time Evaluation of Method Invocation

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐