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

使用方法引用而不是lambda表达式引发java.lang.NullPointerException,java,lambda,nullpointerexception,java-8,method-reference,Java,Lambda,Nullpointerexception,Java 8,Method Reference,我注意到使用Java8方法引用的未处理异常有些奇怪。这是我的代码,使用lambda表达式()->s.toLowerCase(): 它打印“异常”,因此工作正常。但是当我更改线程t以使用方法引用时(甚至IntelliJ也建议): 未捕获异常: 线程“main”java.lang.NullPointerException中的异常 Test.testNPE(Test.java:9) at Test.main(Test.java:4) 在sun.reflect.NativeMethodAccessorI

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

它打印“异常”,因此工作正常。但是当我更改
线程t
以使用方法引用时(甚至IntelliJ也建议):

未捕获异常:

线程“main”java.lang.NullPointerException中的异常 Test.testNPE(Test.java:9) at Test.main(Test.java:4) 在sun.reflect.NativeMethodAccessorImpl.invoke0(本机方法)处 位于sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) 在sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)中 位于java.lang.reflect.Method.invoke(Method.java:497) 位于com.intellij.rt.execution.application.AppMain.main(AppMain.java:144)
有人能解释一下这里发生了什么吗?

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

联合联络小组:

首先,如果方法引用表达式以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表达式。然后附加异常处理程序,线程将启动,抛出异常,异常将被处理程序捕获



作为旁注,EclipseMars.2在这方面似乎有一个小错误:即使使用方法引用,它也会调用异常处理程序。Eclipse没有在应该的时候在
s::toLowerCase
上抛出
NullPointerException
,因此在以后添加异常处理程序时推迟了异常。你发现了一些有趣的事情。让我们看看以下几点:

Function<String, String> stringStringFunction = String::toLowerCase;
下一个

其次是

Runnable r = () -> s.toLowerCase()
它是
Runnable
接口的一个实现,当执行该接口时,将对给定字符串
s
调用方法
toLowerCase

那么你的情况呢

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

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

我已经签入了EclipseMySelf,它运行良好。Ideone.com抛出NPE来检查这一点:lambda在执行时进行求值,函数引用在声明时进行求值。一种更为理论化的阅读方法是:表达式求值为“标准形式”(即值)。碰巧的是,lambdas的范式在被调用之前不会对身体进行评估(也称为弱头范式WHNF)。这意味着值“error”与“()->error”不同,因为后者仅在调用函数时产生错误。这一技巧也可用于用渴望的语言编写不动点组合器:您使用懒惰的组合器
fixf=f(fixf)
,并添加lambda抽象来引入懒散
fixf=x->f(fixf)x
。还可以看到,异常不仅发生在未捕获的异常处理程序设置之前,异常发生在主线程中,而不是
t
“对于lambda表达式,这不会发生,因为lambda将在不执行其主体的情况下进行计算。”这一部分建议方法引用求值与lambda表达式求值相反,并且方法引用求值调用引用的方法。方法引用求值只是有一个额外的步骤来验证子表达式,但验证不是调用的一部分,而是求值的一部分。来自JLS:“方法引用表达式的计算不同于方法本身的调用。”。总之,这篇文章很棒。@Martin其行为与
someObject.new InnerClass()
相同。在语义层面上,当您说
list.iterator()
时,无论您是否使用过迭代器,当
list
null
时,迭代器将立即失败。这适用于所有类型的绑定。虽然
()->s.toLowerCase()
不绑定
s
,但它会在每次计算函数时重新计算它。换句话说,
x->System.out.println(x)
不会绑定
System.out
,但每次都会重新评估它,反映有人同时调用
System.setOut
。与
System.out::println
“调用
s::toLowerCase
”的结果需要澄清。函数实例的创建不是调用。
Function<String, String> stringStringFunction = String::toLowerCase;
stringStringFunction(param) === param.toLowerCase()
Function<Locale, String> localeStringFunction = s::toLowerCase;
localeStringFunction(locale) === s.toLowerCase(locale)
Runnable r = () -> s.toLowerCase()
Thread t = new Thread(s::toLowerCase);