深入异常
# 深入异常
本文我们来讲解更多关于异常的信息
# 从异常中获取信息
异常对象包含关于异常的有价值的信息。可以利用下面这些 java.lang.Throwable 类中的实例方法获取有关异常的信息:
+getMessage():String
返回描述该异常对象的信息+toString():String
返回三个字符串的连接:异常类的全名,":"冒号和空白,getMessage()
的返回+printStackTrace():void
在控制台上打印 Throwable 对象和它的调用堆栈信息。+getStackTrace():StackTraceElemnet[]
返回和该异常对象相关的代表堆栈跟踪的一个堆栈跟踪元素的数组
我们来演示下上述方法,首先来看这段代码:
public class LearnExceptionDemo3 {
public static void main(String[] args) {
try {
int[] list = {1,2,3,4,5};
System.out.println(sum(list));
} catch (Exception ex) {
ex.printStackTrace();
System.out.println("ex.getMessage(): " + ex.getMessage());
System.out.println("ex.toString(): " + ex.toString());
StackTraceElement[] traceElements = ex.getStackTrace();
for (int i = 0; i < traceElements.length; i++) {
System.out.print("method: " + traceElements[i].getMethodName() + ". ");
System.out.print("class: " + traceElements[i].getClassName() + ". ");
System.out.println("LineNumber: " + traceElements[i].getLineNumber() +". ");
}
}
}
static int sum(int[] list){
int result = 0;
for (int i = 0; i <= list.length; i++) {
result += list[i];
}
return result;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
在 sum 方法里,求和的时候,由于数组越界,会抛出一个异常,然后被 main 方法的 try-catch 块处理。
运行结果:
$ javac LearnExceptionDemo3.java
$ java LearnExceptionDemo3
java.lang.ArrayIndexOutOfBoundsException: 5
at LearnExceptionDemo3.sum(LearnExceptionDemo3.java:24)
at LearnExceptionDemo3.main(LearnExceptionDemo3.java:5)
ex.getMessage(): 5
ex.toString(): java.lang.ArrayIndexOutOfBoundsException: 5
method: sum. class: LearnExceptionDemo3. LineNumber: 24.
method: main. class: LearnExceptionDemo3. LineNumber: 5.
2
3
4
5
6
7
8
9
我们依次来分析下。首先 ex.printStackTrace()
的输出如下:
java.lang.ArrayIndexOutOfBoundsException: 5
at LearnExceptionDemo3.sum(LearnExceptionDemo3.java:24)
at LearnExceptionDemo3.main(LearnExceptionDemo3.java:5)
2
3
第一行说明了异常的类型,从名字可以看出是数组越界了,并且贴心的告诉你越界的下标是 5. 注意,不同异常的输出也不同,例如如果输出为 0,则打印的字符串是这样子的:java.lang.ArithmeticException: / by zero
,也就是会告诉具体是为什么抛出这个异常(除以 0 了)。
第二行就是具体哪个方法抛出的。
第三行就是哪个方法调用了抛出异常的代码(注意,可能不止 3 行,这得看方法调用链有多少个)
从下往上看,可以看到调用的层次,这里是 main 调用了 sum 方法
ex.getMessage()
的输出为 5,这个就是越界的下标
ex.toString()
的输出:
java.lang.ArrayIndexOutOfBoundsException: 5
就是 3 个字符串:异常类的全名
+ ":"
+getMessage()
,也不用过多解释
接下来我们来看看 ex.getStackTrace();
,其返回一个 StackTraceElement
的数组。那么什么是 StackTraceElement
呢?
StackTrace(堆栈轨迹)存放的就是方法调用栈的信息,每次调用一个方法会产生一个方法栈,当前方法调用另外一个方法时会使用栈将当前方法的现场信息保存在此方法栈当中,获取这个栈就可以得到方法调用的详细过程。
然后我们打印其中一个 StackTraceElement
的信息,可以得到调用方法名、类名和发生异常的行号,输出如下:
method: sum. class: LearnExceptionDemo3. LineNumber: 24.
method: main. class: LearnExceptionDemo3. LineNumber: 5.
2
# finally
有时候,不论异常是否出现或者是否被捕获,都希望执行某些代码,例如关闭数据库连接,或者关闭 IO 流。Java 有一个 finally 子句,可以用来达到这个目的,格式如下:
try {
statements;
}
catch(TheException ex){
handling ex;
}
finally {
finalStatements;
}
2
3
4
5
6
7
8
9
使用 finally 子句时,可以省略掉 catch 块。
在任何情况下,finally 块中的代码都会执行,不论 try 块中是否出现异常或者是否被捕获:
- 如果 try 块中没有出现异常,执行 finalStatements, 然后执行 try 块的下一条语句。
- 如果 try 块中有一条语句引起异常,并被 catch 块捕获,则执行 catch 块和 finally 子句。然后执行 try 块之后的下一条语句。
- 如果 try 块中有一条语句引起异常,但是没有被任何 catch 块捕获,就会跳过 try 块中的其他语句,执行 finally 语句,并且将异常传递给这个方法的调用者。
- 即使在到达 finally 块之前有一个 return 语句,finally 块还是会执行。
# 异常屏蔽
如果在执行 finally
语句时抛出异常,那么,catch
语句的异常还能否继续抛出?例如:
public class LearnExceptionDemo4 {
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();
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
执行上述代码,发现异常信息如下:
catched
finally
Exception in thread "main" java.lang.IllegalArgumentException
at LearnExceptionDemo4.main(LearnExceptionDemo4.java:10)
2
3
4
这说明 finally
抛出异常后,原来在 catch
中准备抛出的 RuntimeException
异常就“消失”了,因为只能抛出一个异常。没有被抛出的异常称为“被屏蔽”的异常(Suppressed Exception)。
在极少数的情况下,我们需要获知所有的异常。如何保存所有的异常信息?方法是先用一个 origin
变量保存原始异常,然后调用 Throwable.addSuppressed()
,把原始异常添加进来,最后在 finally
抛出:
public class LearnExceptionDemo5 {
public static void main(String[] args) throws Exception{
Exception origin = null;
try {
Integer.parseInt("abc");
} catch (Exception e) {
origin = e;
throw e;
}finally {
Exception e = new IllegalArgumentException();
if(null != origin){
e.addSuppressed(origin);
}
throw e;
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
运行结果:
Exception in thread "main" java.lang.IllegalArgumentException
at LearnExceptionDemo5.main(LearnExceptionDemo5.java:10)
Suppressed: java.lang.NumberFormatException: For input string: "abc"
at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
at java.lang.Integer.parseInt(Integer.java:580)
at java.lang.Integer.parseInt(Integer.java:615)
at LearnExceptionDemo5.main(LearnExceptionDemo5.java:5)
2
3
4
5
6
7
当 catch
和 finally
都抛出了异常时,虽然 catch
的异常被屏蔽了,但是,finally
抛出的异常仍然包含了它。
通过 Throwable.getSuppressed()
可以获取所有的 Suppressed Exception
。
绝大多数情况下,在 finally
中不要抛出异常。因此,我们通常不需要关心 Suppressed Exception
。
# 使用异常的优点和缺点
优点:try 块包含正常情况下执行的代码。catch 块包含异常情况下执行的代码。异常处理将错误处理代码从正常的程序设计任务中分离出来,这样,可以使程序更易读、更易修改。
缺点:由于异常处理需要初始化新的异常对象,需要从调用栈返回,而且还需要沿着方法调用链来传播异常以便找到它的异常处理器,所以,异常处理通常需要更多的时间和资源。
那么什么时候应该使用异常呢?如果想让该方法的调用者处理异常,应该创建一个异常对象并将其抛出。如果能在发生异常的方法中处理异常,那么就不需要抛出或使用异常。 — 般来说,一个项目中多个类都会发生的共同异常应该考虑作为一种异常类。对于发生在个别方法中的简单错误最好进行局部处理,无须抛出异常。
一句话:当错误需要被方法的调用者处理的时候,方法应该抛出一个异常。
# 重新抛出异常
如果异常处理器不能处理一个异常,或者只是简单地希望它的调用者注意到该异常,Java 允许该异常处理器重新抛出异常,格式如下:
try {
statements;
}
catch(TheException ex){
perform operations before exits;
throw ex;
}
2
3
4
5
6
7
语句 throw ex 重新抛出异常给调用者,以便调用者的其他处理器获得处理异常 ex 的机会。
# 链式异常
链式异常是我们工作中经常会遇到的,请读者好好掌握。
在上一小节,我们让 catch 块重新抛出原始的异常。
但有时候,可能需要同原始异常一起抛出一个新异常(带有附加信息),这称为链式异常( chained exception )。我们来看下面的代码:
public class ChainedExceptionDemo {
public static void main(String[] args) {
try {
method1();
}
catch (Exception ex) {
ex.printStackTrace();
}
}
public static void method1() throws Exception {
try {
method2();
}
catch (Exception ex) {
throw new Exception("New info from method1", ex);
}
}
public static void method2() throws Exception {
throw new Exception("New info from method2");
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
我们来分析下运行结果:
- main 方法调用 method1
- method1 调用 method2
- method2 抛出一个异常,该异常被 method1 的 catch 块捕获
- method1 将异常包装成一个新的异常,并且抛出
- main 方法捕获该异常,并输出
运行结果:
$ javac ChainedExceptionDemo.java
$ java ChainedExceptionDemo
java.lang.Exception: New info from method1
at ChainedExceptionDemo.method1(ChainedExceptionDemo.java:16)
at ChainedExceptionDemo.main(ChainedExceptionDemo.java:4)
Caused by: java.lang.Exception: New info from method2
at ChainedExceptionDemo.method2(ChainedExceptionDemo.java:21)
at ChainedExceptionDemo.method1(ChainedExceptionDemo.java:13)
... 1 more
2
3
4
5
6
7
8
9
可以看到,首先输出 method1 抛出的新异常,然后显示 method2 抛出的新异常
注意到 Caused by: Xxx
在 method2 的那里,说明 method1 里捕获的 Exception
并不是造成问题的根源,根源在于 method2
的 Exception
,是在 method2()
方法抛出的。
throw new Exception("New info from method1", ex);
,其实内部调用的就是 initCause 方法:
catch (Exception ex) {
Exception ex2 = new Exception("New info from method1");
ex2.initCause(ex);
throw ex2;
}
2
3
4
5
运行结果:
java.lang.Exception: New info from method1
at chapter10Exception.ChainedExceptionDemo2.method1(ChainedExceptionDemo2.java:18)
at chapter10Exception.ChainedExceptionDemo2.main(ChainedExceptionDemo2.java:6)
Caused by: java.lang.Exception: New info from method2
at chapter10Exception.ChainedExceptionDemo2.method2(ChainedExceptionDemo2.java:25)
at chapter10Exception.ChainedExceptionDemo2.method1(ChainedExceptionDemo2.java:15)
... 1 more
2
3
4
5
6
7
因此,我们一般直接使用 throw new Exception("New info from method1", ex);
即可,较少使用 initCause
本文我们来讲解更多关于异常的信息
# 从异常中获取信息
异常对象包含关于异常的有价值的信息。可以利用下面这些 java.lang.Throwable 类中的实例方法获取有关异常的信息:
+getMessage():String
返回描述该异常对象的信息+toString():String
返回三个字符串的连接:异常类的全名,":"冒号和空白,getMessage()
的返回+printStackTrace():void
在控制台上打印Throwable对象和它的调用堆栈信息。+getStackTrace():StackTraceElemnet[]
返回和该异常对象相关的代表堆栈跟踪的一个堆栈跟踪元素的数组
我们来演示下上述方法,首先来看这段代码:
public class LearnExceptionDemo3 {
public static void main(String[] args) {
try {
int[] list = {1,2,3,4,5};
System.out.println(sum(list));
} catch (Exception ex) {
ex.printStackTrace();
System.out.println("ex.getMessage(): " + ex.getMessage());
System.out.println("ex.toString(): " + ex.toString());
StackTraceElement[] traceElements = ex.getStackTrace();
for (int i = 0; i < traceElements.length; i++) {
System.out.print("method: " + traceElements[i].getMethodName() + ". ");
System.out.print("class: " + traceElements[i].getClassName() + ". ");
System.out.println("LineNumber: " + traceElements[i].getLineNumber() +". ");
}
}
}
static int sum(int[] list){
int result = 0;
for (int i = 0; i <= list.length; i++) {
result += list[i];
}
return result;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
在sum方法里,求和的时候,由于数组越界,会抛出一个异常,然后被main方法的try-catch块处理。
运行结果:
$ javac LearnExceptionDemo3.java
$ java LearnExceptionDemo3
java.lang.ArrayIndexOutOfBoundsException: 5
at LearnExceptionDemo3.sum(LearnExceptionDemo3.java:24)
at LearnExceptionDemo3.main(LearnExceptionDemo3.java:5)
ex.getMessage(): 5
ex.toString(): java.lang.ArrayIndexOutOfBoundsException: 5
method: sum. class: LearnExceptionDemo3. LineNumber: 24.
method: main. class: LearnExceptionDemo3. LineNumber: 5.
2
3
4
5
6
7
8
9
我们依次来分析下。首先ex.printStackTrace()
的输出如下:
java.lang.ArrayIndexOutOfBoundsException: 5
at LearnExceptionDemo3.sum(LearnExceptionDemo3.java:24)
at LearnExceptionDemo3.main(LearnExceptionDemo3.java:5)
2
3
第一行说明了异常的类型,从名字可以看出是数组越界了,并且贴心的告诉你越界的下标是5. 注意,不同异常的输出也不同,例如如果输出为0,则打印的字符串是这样子的:java.lang.ArithmeticException: / by zero
,也就是会告诉具体是为什么抛出这个异常(除以0了)。
第二行就是具体哪个方法抛出的。
第三行就是哪个方法调用了抛出异常的代码(注意,可能不止3行,这得看方法调用链有多少个)
从下往上看,可以看到调用的层次,这里是main调用了sum方法
ex.getMessage()
的输出为5,这个就是越界的下标
ex.toString()
的输出:
java.lang.ArrayIndexOutOfBoundsException: 5
就是3个字符串:异常类的全名
+ ":"
+getMessage()
,也不用过多解释
接下来我们来看看ex.getStackTrace();
,其返回一个StackTraceElement
的数组。那么什么是StackTraceElement
呢?
StackTrace(堆栈轨迹)存放的就是方法调用栈的信息,每次调用一个方法会产生一个方法栈,当前方法调用另外一个方法时会使用栈将当前方法的现场信息保存在此方法栈当中,获取这个栈就可以得到方法调用的详细过程。
然后我们打印其中一个StackTraceElement
的信息,可以得到调用方法名、类名和发生异常的行号,输出如下:
method: sum. class: LearnExceptionDemo3. LineNumber: 24.
method: main. class: LearnExceptionDemo3. LineNumber: 5.
2
# finally
有时候,不论异常是否出现或者是否被捕获,都希望执行某些代码,例如关闭数据库连接,或者关闭IO流。Java 有一个finally 子句,可以用来达到这个目的,格式如下:
try {
statements;
}
catch(TheException ex){
handling ex;
}
finally {
finalStatements;
}
2
3
4
5
6
7
8
9
使用 finally 子句时,可以省略掉 catch 块。
在任何情况下,finally 块中的代码都会执行,不论 try 块中是否出现异常或者是否被捕获:
- 如果 try 块中没有出现异常,执行 finalStatements, 然后执行 try 块的下一条语句。
- 如果 try 块中有一条语句引起异常,并被 catch 块捕获,则执行 catch 块和 finally 子句。然后执行 try 块之后的下一条语句。
- 如果 try 块中有一条语句引起异常,但是没有被任何 catch 块捕获,就会跳过 try 块中的其他语句,执行 finally 语句,并且将异常传递给这个方法的调用者。
- 即使在到达 finally 块之前有一个 return 语句,finally 块还是会执行。
# 异常屏蔽
如果在执行finally
语句时抛出异常,那么,catch
语句的异常还能否继续抛出?例如:
public class LearnExceptionDemo4 {
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();
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
执行上述代码,发现异常信息如下:
catched
finally
Exception in thread "main" java.lang.IllegalArgumentException
at LearnExceptionDemo4.main(LearnExceptionDemo4.java:10)
2
3
4
这说明finally
抛出异常后,原来在catch
中准备抛出的RuntimeException
异常就“消失”了,因为只能抛出一个异常。没有被抛出的异常称为“被屏蔽”的异常(Suppressed Exception)。
在极少数的情况下,我们需要获知所有的异常。如何保存所有的异常信息?方法是先用一个origin
变量保存原始异常,然后调用Throwable.addSuppressed()
,把原始异常添加进来,最后在finally
抛出:
public class LearnExceptionDemo5 {
public static void main(String[] args) throws Exception{
Exception origin = null;
try {
Integer.parseInt("abc");
} catch (Exception e) {
origin = e;
throw e;
}finally {
Exception e = new IllegalArgumentException();
if(null != origin){
e.addSuppressed(origin);
}
throw e;
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
运行结果:
Exception in thread "main" java.lang.IllegalArgumentException
at LearnExceptionDemo5.main(LearnExceptionDemo5.java:10)
Suppressed: java.lang.NumberFormatException: For input string: "abc"
at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
at java.lang.Integer.parseInt(Integer.java:580)
at java.lang.Integer.parseInt(Integer.java:615)
at LearnExceptionDemo5.main(LearnExceptionDemo5.java:5)
2
3
4
5
6
7
当catch
和finally
都抛出了异常时,虽然catch
的异常被屏蔽了,但是,finally
抛出的异常仍然包含了它
通过Throwable.getSuppressed()
可以获取所有的Suppressed Exception
。
绝大多数情况下,在finally
中不要抛出异常。因此,我们通常不需要关心Suppressed Exception
。
# 使用异常的优点和缺点
优点:try 块包含正常情况下执行的代码。catch 块包含异常情况下执行的代码。异常处理将错误处理代码从正常的程序设计任务中分离出来,这样,可以使程序更易读、更易修改。
缺点:由于异常处理需要初始化新的异常对象,需要从调用栈返回,而且还需要沿着方法调用链来传播异常以便找到它的异常处理器,所以,异常处理通常需要更多的时间和资源。
那么什么时候应该使用异常呢?如果想让该方法的调用者处理异常,应该创建一个异常对象并将其抛出。如果能在发生异常的方法中处理异常,那么就不需要抛出或使用异常。 — 般来说,一个项目中多个类都会发生的共同异常应该考虑作为一种异常类。对于发生在个别方法中的简单错误最好进行局部处理,无须抛出异常。
一句话:当错误需要被方法的调用者处理的时候,方法应该抛出一个异常。
# 重新抛出异常
如果异常处理器不能处理一个异常,或者只是简单地希望它的调用者注意到该异常,Java 允许该异常处理器重新抛出异常,格式如下:
try {
statements;
}
catch(TheException ex){
perform operations before exits;
throw ex;
}
2
3
4
5
6
7
语句 throw ex 重新抛出异常给调用者,以便调用者的其他处理器获得处理异常 ex 的机会。
# 链式异常
链式异常是我们工作中经常会遇到的,请读者好好掌握。
在上一小节,我们让 catch 块重新抛出原始的异常。
但有时候,可能需要同原始异常一起抛出一个新异常(带有附加信息),这称为链式异常( chained exception )。我们来看下面的代码:
public class ChainedExceptionDemo {
public static void main(String[] args) {
try {
method1();
}
catch (Exception ex) {
ex.printStackTrace();
}
}
public static void method1() throws Exception {
try {
method2();
}
catch (Exception ex) {
throw new Exception("New info from method1", ex);
}
}
public static void method2() throws Exception {
throw new Exception("New info from method2");
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
我们来分析下运行结果:
- main方法调用method1
- method1调用method2
- method2抛出一个异常,该异常被method1的catch块捕获
- method1将异常包装成一个新的异常,并且抛出
- main方法捕获该异常,并输出
运行结果:
$ javac ChainedExceptionDemo.java
$ java ChainedExceptionDemo
java.lang.Exception: New info from method1
at ChainedExceptionDemo.method1(ChainedExceptionDemo.java:16)
at ChainedExceptionDemo.main(ChainedExceptionDemo.java:4)
Caused by: java.lang.Exception: New info from method2
at ChainedExceptionDemo.method2(ChainedExceptionDemo.java:21)
at ChainedExceptionDemo.method1(ChainedExceptionDemo.java:13)
... 1 more
2
3
4
5
6
7
8
9
可以看到,首先输出method1抛出的新异常,然后显示method2抛出的新异常
注意到Caused by: Xxx
在method2的那里,说明method1里捕获的Exception
并不是造成问题的根源,根源在于method2
的Exception
,是在method2()
方法抛出的。
throw new Exception("New info from method1", ex);
,其实内部调用的就是initCause方法:
catch (Exception ex) {
Exception ex2 = new Exception("New info from method1");
ex2.initCause(ex);
throw ex2;
}
2
3
4
5
运行结果:
java.lang.Exception: New info from method1
at chapter10Exception.ChainedExceptionDemo2.method1(ChainedExceptionDemo2.java:18)
at chapter10Exception.ChainedExceptionDemo2.main(ChainedExceptionDemo2.java:6)
Caused by: java.lang.Exception: New info from method2
at chapter10Exception.ChainedExceptionDemo2.method2(ChainedExceptionDemo2.java:25)
at chapter10Exception.ChainedExceptionDemo2.method1(ChainedExceptionDemo2.java:15)
... 1 more
2
3
4
5
6
7
因此,我们一般直接使用throw new Exception("New info from method1", ex);
即可,较少使用initCause