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!"));
即使s
是null
,也会正确创建 lambda 表达式。然后将附加异常处理程序,线程将启动,抛出异常,将被处理程序捕获。
附带说明一下,Eclipse Mars.2 似乎对此有一个小错误:即使使用方法引用,它也会调用异常处理程序。 Eclipse 没有在应该的时候在 s::toLowerCase
处抛出 NullPointerException
,因此稍后在添加异常处理程序时推迟异常。
【讨论】:
一种更理论化的阅读方式是:表达式被评估为“正常形式”(即值)。碰巧 lambdas' 范式在被调用之前不评估主体(也称为弱头范式 WHNF)。这意味着值“error”与“() -> error”不同,因为后者仅在调用函数时才会产生错误。这个技巧也用于在 Eager 语言中编写定点组合器:你采用惰性组合器fix f = f (fix f)
并添加一个 lambda 抽象来引入惰性 fix f = x -> 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()
时,你是否会使用迭代器也无关紧要,当list
是null
时它会立即失败。这适用于所有类型的绑定。而() -> s.toLowerCase()
不绑定s
,它会在每次评估函数时重新评估它。换句话说,x -> 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;
是从Locale
到String
的函数。这相当于s.toLowerCase(Locale)
方法调用。它在引擎盖下对 2 个参数起作用:一个是 s
,另一个是某些语言环境。如果s
是null
,那么这个函数创建会抛出一个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报"An error has occurred,See error log for more details. java.lang.NullPointerExce