1. 概述

Java 8 引入了方法引用(Method References)这一特性,它与 Lambda 表达式非常相似,也常被开发者混用。

但需要注意的是,方法引用并不完全等同于 Lambda 表达式。本文将从底层机制出发,说明它们之间的差异,并重点分析在使用方法引用时可能遇到的坑点和潜在风险。

2. Lambda 表达式与方法引用的语法对比

我们先来看几个 Lambda 表达式的例子:

Runnable r1 = () -> "some string".toUpperCase();
Consumer<String> c1 = x -> x.toUpperCase();

再来看几个方法引用的例子:

Function<String, String> f1 = String::toUpperCase;
Runnable r2 = "some string"::toUpperCase;
Runnable r3 = String::new;

从这些例子来看,方法引用像是 Lambda 表达式的简化写法。但它们的底层机制并不完全一样。

根据 Oracle 官方文档,Java 允许在 :: 运算符前使用更复杂的表达式。例如:

(test ? list.replaceAll(String::trim) : list) :: iterator

其中,:: 前的部分称为 目标引用(target reference)。接下来我们将重点分析它的求值行为。

3. 方法引用的目标引用求值行为

来看一个例子:

public static void main(String[] args) {
    Runnable runnable = (f("some") + f("string"))::toUpperCase;
}

private static String f(String string) {
    System.out.println(string);
    return string;
}

这段代码看似只是声明了一个 Runnable,但运行后输出却是:

some
string

⚠️这说明:目标引用是在声明时就被求值的,而不是等到调用时才执行。

换句话说,方法引用不像 Lambda 表达式那样具备“惰性求值”的特性。如果我们在目标引用中执行了耗时操作或副作用(如打印、IO、计算等),它会在声明时就立即执行,并且只会执行一次。

再来看一个更典型的踩坑场景:

SomeWorker worker = null;
Runnable workLambda = () -> worker.work(); // ok
Runnable workMethodReference = worker::work; // boom! NullPointerException

上面这段代码中:

  • Lambda 写法是安全的:只有在 run() 被调用时才会访问 worker,此时如果 worker 为 null 才会抛异常。
  • 方法引用则不同:目标引用在声明时就会被求值,因此会立即抛出 NullPointerException

这说明:方法引用的目标引用部分会在声明时被立即求值且仅求值一次。所以,我们应避免在目标引用中使用变量访问或复杂表达式。

✅建议做法是:

  • 方法引用尽量只用于替代简单的 Lambda 表达式。
  • 如果使用,建议目标引用部分只保留类名或简单对象名,避免副作用或延迟执行的逻辑。

例如:

Function<String, String> f1 = String::toUpperCase; // ✅ 推荐

而不是:

Runnable r = someService.getData()::process; // ❌ 不推荐,声明时就执行 getData()

4. 总结

通过本文的分析,我们可以得出以下关键结论:

✅ 方法引用不是 Lambda 表达式的等价替代,它们在求值时机上存在本质区别
✅ 方法引用的目标引用部分会在声明时被立即求值,且只求值一次
✅ 如果目标引用涉及变量或复杂逻辑,可能导致 NPE 或副作用
✅ 推荐用法是:Class::methodinstance::method,避免副作用和提前求值问题

使用方法引用时务必谨慎,避免因求值时机导致程序行为异常。


原始标题:Evaluation of Methods References in Java