Java异常传播

引言:Java异常的传播机制。如何善用try、catch、finally?

Java异常的传播机制

本文不再讲解异常的基本知识点,进入更深层的异常学习,有关基础可以看博客

怎么看打印的异常信息

我们通常知道使用e.printStackTrace()来打印错误信息,但是怎么看呢?

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class TempTest {
public static void main(String[] args) {
try {
process1();
} catch (Exception e) {
e.printStackTrace();
}
}

static void process1() {
process2();
}

static void process2() {
Integer.parseInt(null); // 会抛出NumberFormatException
}
}

运行后报出

1
2
3
4
5
6
7
java.lang.NumberFormatException: null
at java.lang.Integer.parseInt(Integer.java:542)
at java.lang.Integer.parseInt(Integer.java:615)
at TempTest.process2(TempTest.java:20)
at TempTest.process1(TempTest.java:16)
at TempTest.main(TempTest.java:9)
//IDEA中可以点击括号内容对应找到原码

我们可以看出来,方法的运行顺序是

  1. main调用了process1
  2. process1调用了process2
  3. process2调用了Integer.parseInt(String s)
  4. Integer.parseInt(String s调用了public static int parseInt(String s, int radix)

打印结果如上,说明打印的结果是从最底层向最上层,层层打印出来的

点击第一行括号内容,就可以找到抛出异常的原码位置

1
2
3
4
5
6
public static int parseInt(String s, int radix) throws NumberFormatException {
if (s == null) {
throw new NumberFormatException("null");
}
...
}

异常转换

观察以下代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class TempTest {
public static void main(String[] args) {
try {
process1();
} catch (Exception e) {
e.printStackTrace();
}
}

static void process1() {
try {
process2();
} catch (NullPointerException e) {
throw new IllegalArgumentException();
}
}

static void process2() {
throw new NullPointerException();
}
}

分析一下:

  1. main调用process1
  2. process1调用process2
  3. process2抛出NullPointerException
  4. catch捕获异常NullPointerException
  5. process1抛出IllegalArgumentException

运行后的异常栈类似下面:

1
2
3
java.lang.IllegalArgumentException
at TempTest.process1(TempTest.java:19)
at TempTest.main(TempTest.java:9)

发现:打印结果中只有IllegalArgumentException,看不到NullPointerException异常丢失了原始的异常信息

如果开发中出现这种问题,我们将无法定位原始的异常位置,该怎么办呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class TempTest {
public static void main(String[] args) {
try {
process1();
} catch (Exception e) {
e.printStackTrace();
}
}

static void process1() {
try {
process2();
} catch (NullPointerException e) {
throw new IllegalArgumentException(e);
//改动在这里,将e作为了实例穿了进去
}
}

static void process2() {
throw new NullPointerException();
}
}

再看打印信息

1
2
3
4
5
6
7
java.lang.IllegalArgumentException: java.lang.NullPointerException
at TempTest.process1(TempTest.java:19)
at TempTest.main(TempTest.java:9)
Caused by: java.lang.NullPointerException
at TempTest.process2(TempTest.java:24)
at TempTest.process1(TempTest.java:17)
... 1 more

注意到Caused by: Xxx,说明捕获的IllegalArgumentException并不是造成问题的根源,根源在于NullPointerException,是在TempTest.process2()方法抛出的。

在Catch抛出异常

如果我们在try或者catch语句块中抛出异常,finally语句是否会执行?例如:

1
2
3
4
5
6
7
8
9
10
11
12
public class Main {
public static void main(String[] args) {
try {
Integer.parseInt("abc");
} catch (Exception e) {
System.out.println("catched");
throw new RuntimeException(e);
} finally {
System.out.println("finally");
}
}
}

结果如下:

1
2
3
4
5
6
catched
finally
Exception in thread "main" java.lang.RuntimeException: java.lang.NumberFormatException: For input string: "abc"
at Main.main(Main.java:8)
Caused by: java.lang.NumberFormatException: For input string: "abc"
at ...

发现执行顺序如下;

  1. 先执行catch
  2. 再执行finally
  3. 最后抛出catch中的异常

因此,在catch中抛出异常,不会影响finally的执行。JVM会先执行finally,然后抛出异常。

在finally抛出异常:异常屏蔽

如果在执行finally语句时抛出异常,那么,catch语句的异常还能否继续抛出?例如

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Main {
public static void main(String[] args) {
try {
Integer.parseInt("abc");
} catch (Exception e) {
System.out.println("catched");
throw new RuntimeException(e);
} finally {
System.out.println("finally");
throw new IllegalArgumentException();
}
}
}

结果如下:

1
2
3
4
catched
finally
Exception in thread "main" java.lang.IllegalArgumentException
at Main.main(Main.java:11)

发现执行顺序如下;

  1. 先执行catch
  2. 再执行finally
  3. 最后抛出finally中的异常

catch中抛出的异常去哪儿了??

这个异常被屏蔽了(Suppressed Exception):为了让他显示出来,我们可以使用一个实例将异常存起来,最后调用addSuppressed()方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Main {
public static void main(String[] args) throws Exception {
Exception origin = null;
try {
System.out.println(Integer.parseInt("abc"));
} catch (Exception e) {
origin = e;
throw e;
} finally {
Exception e = new IllegalArgumentException();
if (origin != null) {
e.addSuppressed(origin);
// 如果origin异常存在,那么就带上这个异常
}
throw e;
}
}
}

结果

1
2
3
4
5
6
7
Exception in thread "main" java.lang.IllegalArgumentException
at Main.main(Main.java:11)
Suppressed: java.lang.NumberFormatException: For input string: "abc"
at java.base/java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
at java.base/java.lang.Integer.parseInt(Integer.java:652)
at java.base/java.lang.Integer.parseInt(Integer.java:770)
at Main.main(Main.java:6)

发现Suppressed:后面包含了被屏蔽的异常

绝大多数情况下,在finally中不要抛出异常。因此,我们通常不需要关心Suppressed Exception

总结

本节我们可以总结的规则如下:

  • printStackTrace()方法可以打印异常栈信息,打印的顺序是从内到外的(因为方法的调用是入栈)
  • 如果catch代码块抛出了新的异常,尽量把旧的异常作为参数传入新的异常,这样可以避免异常转换,导致追踪不到原始的报错位置
  • 如果catch代码块出现异常,依然会先执行finally代码块
  • 如果finally中抛出异常,那么catch抛出的异常将不再理会,因此如果finally要抛出异常,最好使用addSuppressed()

参考资料:廖雪峰官方网站