使用方法引用时抛出java.lang.NullPointerException异常,但不使用lambda表达式。

60

我注意到在使用Java 8方法引用时,未处理的异常有些奇怪。这是我的代码,使用lambda表达式 () -> s.toLowerCase()

public class Test {

    public static void main(String[] args) {
        testNPE(null);
    }

    private static void testNPE(String s) {
        Thread t = new Thread(() -> s.toLowerCase());
//        Thread t = new Thread(s::toLowerCase);
        t.setUncaughtExceptionHandler((t1, e) -> System.out.println("Exception!"));
        t.start();
    }
}

它打印出“异常”,所以它工作正常。但是当我将 Thread t 更改为使用方法引用(即使 IntelliJ 建议这样做):

Thread t = new Thread(s::toLowerCase);

未捕获异常:

Exception in thread "main" java.lang.NullPointerException
    at Test.testNPE(Test.java:9)
    at Test.main(Test.java:4)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:497)
    at com.intellij.rt.execution.application.AppMain.main(AppMain.java:144)

有人能解释一下这里发生了什么吗?

我自己在Eclipse中检查过,它可以正常工作。Ideone.com也会抛出NPE。 - Krzysztof Majewski
1
请查看此链接:https://ideone.com/nPvWex - FaNaJ
5
当执行时,lambda表达式会被求值,而函数引用则在声明时被求值。 - njzk2
虽然从技术上讲是有道理的,但这样做太狡猾了。 - undefined
2个回答

68
这种行为依赖于方法引用和Lambda表达式的评估过程之间微妙的差异。
来自JLS 方法引用的运行时评估

首先,如果方法引用表达式以ExpressionName或Primary开头,则会对此子表达式进行评估。 如果子表达式评估为null,则引发NullPointerException,并使方法引用表达式中止

使用以下代码:
Thread t = new Thread(s::toLowerCase); // <-- s is null, NullPointerException thrown here
t.setUncaughtExceptionHandler((t1, e) -> System.out.println("Exception!"));

当方法引用被评估时,表达式 s 将被评估为null并抛出异常。然而,在此之前,没有附加异常处理程序,因为该代码将在之后执行。
对于lambda表达式,情况并非如此,因为lambda将在不执行其主体的情况下进行评估。从Lambda表达式的运行时评估中可以看到:

对lambda表达式的评估与执行lambda主体是不同的。

Thread t = new Thread(() -> s.toLowerCase());
t.setUncaughtExceptionHandler((t1, e) -> System.out.println("Exception!"));

即使 snull,lambda表达式也会正确创建。然后将附加异常处理程序,线程将启动,抛出一个异常,该异常将被处理程序捕获。
此外,Eclipse Mars.2似乎存在一个小bug:即使使用方法引用,它仍会调用异常处理程序。 Eclipse没有在应该的时候在s::toLowerCase处抛出NullPointerException,因此推迟了稍后添加异常处理程序时的异常。

7
更理论的阅读方式是:表达式被计算为“正常形式”(即值)。恰好 lambda 的正常形式直到调用时才计算其主体(也称为弱头正常形 WHNF)。这意味着值“error”与“() -> error”不同,因为后者仅在调用函数时产生错误。这个技巧也用于在急切语言中编写固定点组合器:您可以使用惰性组合子 fix f = f (fix f) 并添加一个 lambda 抽象来引入惰性 fix f = x -> f (fix f) x - Bakuriu
16
异常不仅在未设置未捕获异常处理程序之前发生,而且还发生在主线程而不是t线程中。 - Holger
2
这种情况在lambda表达式中不会发生,因为lambda将在执行其主体之前进行评估。这部分表明方法引用评估与lambda表达式评估相反,并且方法引用评估调用引用的方法。方法引用评估只有那个额外的子表达式验证步骤,但是该验证不是调用的一部分,而是评估的一部分。来自JLS:“方法引用表达式的评估与方法本身的调用是不同的。”无论如何,非常棒的文章。 - ctomek
哇,我刚在某段代码中遇到这种情况……我觉得这很奇怪……当对方法引用进行验证时是合乎逻辑的,但是即使在那个时候您根本不知道该方法是否会被执行,声明方法引用时却出现了npe:Thread t = new Thread(s::toLowerCase); if(s!= null) t.start(); 看起来是正确的,只是有些尴尬它与 Thread t = new Thread(() -> s.toLowerCase()); if(s!= null) t.start(); 不等效。然而,JLS 是明确的,但必须要知道… - Martin
1
@Martin 行为与 someObject.new InnerClass() 相同。在语义层面上,当你说 list.iterator() 时,无论你是否使用迭代器,当 listnull 时它都会立即失败。这适用于所有类型的 绑定。而 () -> s.toLowerCase() 不会绑定 s,而是每次函数被评估时重新评估它。换句话说, x -> System.out.println(x) 不会绑定 System.out,而是每次重新评估它,反映了在此期间有人调用了 System.setOut。与 System.out::println 相比。 - Holger

7
哇,你发现了一些有趣的东西。让我们看看以下内容:
Function<String, String> stringStringFunction = String::toLowerCase;

这段代码返回一个函数,接受一个类型为String的参数并返回另一个String,即输入参数的小写形式。这与s.toLowerCase()相当,其中s是输入参数。
stringStringFunction(param) === param.toLowerCase()

下一步
Function<Locale, String> localeStringFunction = s::toLowerCase;

这是一个从 LocaleString 的函数。这相当于调用 s.toLowerCase(Locale) 方法。在底层,它使用了两个参数:一个是 s,另一个是某个区域设置。如果 snull,则此函数创建会抛出一个 NullPointerException
localeStringFunction(locale) === s.toLowerCase(locale)

下一个是
Runnable r = () -> s.toLowerCase()

这是一个实现了Runnable接口的方法,当被执行时,会对给定的字符串s调用toLowerCase方法。

所以在你的情况下

Thread t = new Thread(s::toLowerCase);

尝试创建一个新的线程,将调用`s::toLowerCase`的结果传递给它。但是这会立即抛出一个`NPE`,甚至在线程启动之前就抛出了。因此,在当前线程中抛出了`NPE`,而不是在线程`t`内部抛出。这就是为什么您的异常处理程序没有被执行的原因。

5
需要澄清“调用s::toLowerCase的结果”的含义。创建函数实例并不是调用。 - Holger

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接