我们习惯于说异常的堆栈跟踪反映了“出现异常的位置”,但这是一个不精确的说法。异常的堆栈跟踪通常反映了其实例被创建的位置。
当我们有如下形式的代码时,
1 String s=null;
2 s.length();
当我们尝试对null
进行解引用并调用length()
方法时,JRE将创建一个NullPointerException
实例,因此其堆栈跟踪将报告第2
行。
然而,当我们有如下代码时:
1 String s=null;
2 if(s == null) {
3 RuntimeException rt=new NullPointerException();
4 throw rt;
5 }
堆栈跟踪将不会报告出现错误条件的位置(第2
行)和异常抛出的位置(第4
行),而是报告了实例创建的位置,即第3
行。
对于大多数实际情况来说,这些位置足够接近,没有显著差异,但在这里,我们有一个非同寻常的情况。
正如tonakai指出的那样,ForkJoinTask
将通过反射创建一个已经遇到的异常的新实例,就像我们可以从源代码中看到的那样,当线程不匹配时。
当成功时,它的堆栈跟踪将精确反映新异常实例的创建位置,即在执行反射实例创建的一些生成代码中。当然,这种成功的创建不能区分当JRE由于执行相同代码时发生错误条件时创建异常的情况。
但是,当我们仔细查看源代码时,我们会发现整个反射创建被包含在一个中。
584 try {
…
604 } catch (Exception ignore) {
605 }
代码块。因此,如果操作确实失败了,就不会显示任何异常。相反,代码已经跌倒返回原始异常。这表明反射代码没有失败,而是我们看到通过getThrowableException()
反射成功创建的NullPointerException
实例被返回,并在稍后由ForkJoinTask
故意抛出以报告在处理期间另一个线程中存在NullPointerException
。
但是,此代码将新异常的cause初始化为指向原始异常。例如,以下代码:
import java.util.stream.IntStream;
public class Main
{
public static void main(String[] args) {
Thread main=Thread.currentThread();
IntStream.range(0, 1000).parallel().forEach(i -> {
if(Thread.currentThread()!=main)
throw new NullPointerException();
});
}
}
打印
Exception in thread "main" java.lang.NullPointerException
at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
at java.util.concurrent.ForkJoinTask.getThrowableException(ForkJoinTask.java:598)
at java.util.concurrent.ForkJoinTask.reportException(ForkJoinTask.java:677)
at java.util.concurrent.ForkJoinTask.invoke(ForkJoinTask.java:735)
at java.util.stream.ForEachOps$ForEachOp.evaluateParallel(ForEachOps.java:160)
at java.util.stream.ForEachOps$ForEachOp$OfInt.evaluateParallel(ForEachOps.java:189)
at java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:233)
at java.util.stream.IntPipeline.forEach(IntPipeline.java:404)
at java.util.stream.IntPipeline$Head.forEach(IntPipeline.java:560)
at Main.main(Main.java:7)
Caused by: java.lang.NullPointerException
at Main.lambda$main$0(Main.java:9)
at java.util.stream.ForEachOps$ForEachOp$OfInt.accept(ForEachOps.java:205)
at java.util.stream.Streams$RangeIntSpliterator.forEachRemaining(Streams.java:110)
at java.util.Spliterator$OfInt.forEachRemaining(Spliterator.java:693)
at java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:481)
at java.util.stream.ForEachOps$ForEachTask.compute(ForEachOps.java:291)
at java.util.concurrent.CountedCompleter.exec(CountedCompleter.java:731)
at java.util.concurrent.ForkJoinTask.doExec(ForkJoinTask.java:289)
at java.util.concurrent.ForkJoinPool$WorkQueue.runTask(ForkJoinPool.java:1056)
at java.util.concurrent.ForkJoinPool.runWorker(ForkJoinPool.java:1692)
at java.util.concurrent.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:157)
所以您仍然能够识别发生了什么。您只需要注意原因。由于您问题中的堆栈跟踪看起来不像典型的Throwable.printStackTrace()
输出,可能是产生此输出的代码忽略了异常的cause属性。
作为补充,我们可以使用自定义异常类型来检查如果那个重现确实失败会发生什么:
import java.util.stream.IntStream;
public class Main
{
public static class CustomException extends RuntimeException {
public CustomException() {
System.err.println("will deliberately fail");
throw new NullPointerException();
}
private CustomException(String message) {
super(message);
}
}
public static void main(String[] args) {
Thread main=Thread.currentThread();
IntStream.range(0, 1000).parallel().forEach(i -> {
if(Thread.currentThread()!=main)
throw new CustomException("forced failure");
});
}
}
将会打印
will deliberately fail
Exception in thread "main" Main$CustomException: forced failure
at Main.lambda$main$0(Main.java:18)
at java.util.stream.ForEachOps$ForEachOp$OfInt.accept(ForEachOps.java:205)
at java.util.stream.Streams$RangeIntSpliterator.forEachRemaining(Streams.java:110)
at java.util.Spliterator$OfInt.forEachRemaining(Spliterator.java:693)
at java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:481)
at java.util.stream.ForEachOps$ForEachTask.compute(ForEachOps.java:291)
at java.util.concurrent.CountedCompleter.exec(CountedCompleter.java:731)
at java.util.concurrent.ForkJoinTask.doExec(ForkJoinTask.java:289)
at java.util.concurrent.ForkJoinPool$WorkQueue.runTask(ForkJoinPool.java:1056)
at java.util.concurrent.ForkJoinPool.runWorker(ForkJoinPool.java:1692)
at java.util.concurrent.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:157)
通过默认构造函数反射重建时抛出的NullPointerException
未被报告,并且直接抛出来自其他线程的原始异常。