java.lang.NullPointerException 是使用方法引用而不是 lambda 表达式引发的

Posted

技术标签:

【中文标题】java.lang.NullPointerException 是使用方法引用而不是 lambda 表达式引发的【英文标题】:java.lang.NullPointerException is thrown using a method-reference but not a lambda expression 【发布时间】:2016-09-21 15:20:48 【问题描述】:

我注意到使用 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 看看这个:ideone.com/nPvWex lambdas 在执行时被评估,函数引用在声明时被评估。 【参考方案1】:

这种行为依赖于方法引用和 lambda 表达式的求值过程之间的细微差别。

来自 JLS Run-Time Evaluation of Method References:

首先,如果方法引用表达式以 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 将在没有执行其主体的情况下进行计算。来自Run-Time Evaluation of Lambda Expressions:

lambda 表达式的求值不同于 lambda 主体的执行。

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

即使snull,也会正确创建 lambda 表达式。然后将附加异常处理程序,线程将启动,抛出异常,将被处理程序捕获。


附带说明一下,Eclipse Mars.2 似乎对此有一个小错误:即使使用方法引用,它也会调用异常处理程序。 Eclipse 没有在应该的时候在 s::toLowerCase 处抛出 NullPointerException,因此稍后在添加异常处理程序时推迟异常。

【讨论】:

一种更理论化的阅读方式是:表达式被评估为“正常形式”(即值)。碰巧 lambdas' 范式在被调用之前评估主体(也称为弱头范式 WHNF)。这意味着值“error”与“() -> error”不同,因为后者仅在调用函数时才会产生错误。这个技巧也用于在 Eager 语言中编写定点组合器:你采用惰性组合器 fix f = f (fix f) 并添加一个 lambda 抽象来引入惰性 fix f = x -&gt; f (fix f) x 另见“What is the equivalent lambda expression for System.out::println 异常不仅在设置未捕获异常处理程序之前发生,异常发生在主线程而不是t "在 lambda 表达式的情况下不会发生这种情况,因为 lambda 将在其主体未执行的情况下进行评估。"这部分表明方法引用评估与 lambda 表达式评估相反,并且方法引用评估调用引用的方法。方法引用评估只是有一个额外的验证子表达式的步骤,但验证不是调用的一部分,而是评估的一部分。来自 JLS:“方法引用表达式的评估与方法本身的调用不同。”。无论如何,很棒的帖子。 @Martin 的行为与someObject.new InnerClass() 相同。在语义层面上,当你说list.iterator() 时,你是否会使用迭代器也无关紧要,当listnull 时它会立即失败。这适用于所有类型的绑定。而() -&gt; s.toLowerCase() 不绑定s,它会在每次评估函数时重新评估它。换句话说,x -&gt; System.out.println(x) 不绑定System.out,而是每次都重新评估它,反映同时有人调用System.setOut。与System.out::println 相比。【参考方案2】:

哇。你发现了一些有趣的东西。让我们看看以下内容:

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) 方法调用。它在引擎盖下对 2 个参数起作用:一个是 s,另一个是某些语言环境。如果snull,那么这个函数创建会抛出一个NullPointerException

localeStringFunction(locale) === s.toLowerCase(locale)

接下来是

Runnable r = () -> s.toLowerCase()

这是Runnable 接口的一个实现,当它被执行时,将在给定的字符串s 上调用方法toLowerCase

所以你的情况

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

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

【讨论】:

s::toLowerCase 的调用结果”需要澄清。函数实例的创建不是调用。

以上是关于java.lang.NullPointerException 是使用方法引用而不是 lambda 表达式引发的的主要内容,如果未能解决你的问题,请参考以下文章

亲測,Eclipse报&quot;An error has occurred,See error log for more details. java.lang.NullPointerExce

来自 LayerDrawable 的异常

Microsoft Access 和 Java JDBC-ODBC 错误

如何使用其id获取单个SQLite行数据?