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

Krz*_*ski 44 java lambda nullpointerexception java-8 method-reference

我注意到使用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();
    }
}
Run Code Online (Sandbox Code Playgroud)

它打印"Exception",所以它工作正常.但是当我Thread t改为使用方法引用时(甚至IntelliJ建议):

Thread t = new Thread(s::toLowerCase);
Run Code Online (Sandbox Code Playgroud)

异常没有被捕获:

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)
Run Code Online (Sandbox Code Playgroud)

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

Tun*_*aki 54

此行为依赖于方法引用和lambda表达式的评估过程之间的细微差别.

方法参考的JLS 运行时评估:

首先,如果方法引用表达式以ExpressionName或Primary开头,则计算此子表达式.如果子表达式求值为null,NullPointerException则引发a,并且方法引用表达式突然完成.

使用以下代码:

Thread t = new Thread(s::toLowerCase); // <-- s is null, NullPointerException thrown here
t.setUncaughtExceptionHandler((t1, e) -> System.out.println("Exception!"));
Run Code Online (Sandbox Code Playgroud)

表达式s被计算到,null并且在评估该方法引用时将完全抛出异常.但是,那时没有附加异常处理程序,因为此代码将在之后执行.

在lambda表达式的情况下不会发生这种情况,因为lambda将在不执行其主体的情况下进行计算.从Lambda表达式的运行时评估:

lambda表达式的评估不同于lambda体的执行.

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

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


作为一个侧面说明,似乎Eclipse的Mars.2有这方面一个小bug:即使有方法参考,它调用异常处理程序.Eclipse NullPointerExceptions::toLowerCase应该的时候不会抛出它,因此在添加异常处理程序之后推迟异常.

  • 异常不仅在设置了未捕获异常处理程序之前发生,异常发生在主线程而不是`t`. (14认同)
  • 阅读这个的更理论的方法是:表达式被评估为"正常形式"(即值).碰巧*lambdas'*正常形式在被调用之前不会*评估身体(也称为弱头正常形式WHNF).这意味着值"error"与"() - > error"不同,因为后者仅在调用函数时才产生错误.这个技巧也用于在急切语言中编写fixpoint组合器:你采用懒惰的组合子`fix f = f(fix f)`并添加一个lambda抽象来引入懒惰`fix f = x - > f(fix f)x` . (4认同)
  • 另请参见["什么是`System.out :: println`的等效lambda表达式"](http://stackoverflow.com/a/28025717/2711488) (4认同)
  • "在lambda表达式的情况下不会发生这种情况,因为lambda将在不执行其主体的情况下进行计算." 这部分表明方法参考评估与lambda表达式评估相反,方法参考评估调用参考方法.方法参考评估只是具有非常表达子表达的附加步骤,但该验证不是调用的一部分,但它是评估的一部分.来自JLS:"方法引用表达式的评估不同于方法本身的调用." 无论如何,伟大的帖子. (2认同)

Nik*_*kem 7

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

Function<String, String> stringStringFunction = String::toLowerCase;
Run Code Online (Sandbox Code Playgroud)

这返回一个函数,它接受on类型的参数String并返回另一个函数String,它是输入参数的小写.这有点相当于s.toLowerCase(),其中s是输入参数.

stringStringFunction(param) === param.toLowerCase()
Run Code Online (Sandbox Code Playgroud)

下一个

Function<Locale, String> localeStringFunction = s::toLowerCase;
Run Code Online (Sandbox Code Playgroud)

是从函数LocaleString.这相当于s.toLowerCase(Locale)方法调用.它在引擎盖下有两个参数:一个是s另一个是某个区域设置.如果snull,那么这个函数创建抛出一个NullPointerException.

localeStringFunction(locale) === s.toLowerCase(locale)
Run Code Online (Sandbox Code Playgroud)

接下来是

Runnable r = () -> s.toLowerCase()
Run Code Online (Sandbox Code Playgroud)

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

所以在你的情况下

Thread t = new Thread(s::toLowerCase);
Run Code Online (Sandbox Code Playgroud)

尝试创建一个新的Thread传递调用的结果s::toLowerCase.但这会NPE立刻引发.甚至在线程启动之前.因此NPE抛出当前线程,而不是内部线程t.这就是您的异常处理程序未执行的原因.

  • "调用`s :: toLowerCase`的结果"需要澄清.函数实例的创建不是调用. (4认同)