1 异常处理
异常处理的两大组成要素是抛出异常和捕获异常。这两大要素共同实现程序控制流的非正常转移。
2 抛出异常
抛出异常可分为显式和隐式两种。
-
显式抛异常的主体是应用程序,它指的是在程序中使用“throw”关键字,手动将异常实例抛出。
-
隐式抛异常的主体则是 Java 虚拟机,它指的是 Java 虚拟机在执行过程中,碰到无法继续执行的异常状态,自动抛出异常。举例来说,Java 虚拟机在执行读取数组操作时,发现输入的索引值是负数,故而抛出数组索引越界异常(ArrayIndexOutOfBoundsException)。
3 捕获异常
捕获异常则涉及了如下三种代码块。
- try 代码块:用来标记需要进行异常监控的代码。
- catch 代码块:跟在 try 代码块之后,用来捕获在 try 代码块中触发的某种指定类型的异常。除了声明所捕获异常的类型之外,catch 代码块还定义了针对该异常类型的异常处理器。在 Java 中,try 代码块后面可以跟着多个 catch 代码块,来捕获不同类型的异常。Java 虚拟机会从上至下匹配异常处理器。因此,前面的 catch 代码块所捕获的异常类型不能覆盖后边的,否则编译器会报错。
- finally 代码块:跟在 try 代码块和 catch 代码块之后,用来声明一段必定运行的代码。它的设计初衷是为了避免跳过某些关键的清理代码,例如关闭已打开的系统资源。
3.1 案例
public class TryCatchFinally {
private String tryBlock = "tryBlock";
private String catchBlock = "catchBlock";
private String finallyBlock = "finallyBlock";
public String test() {
try {
System.out.println(tryBlock);
//int a = 5/0; //异常抛出点
return tryBlock;
} catch (Exception e) {
//int a = 5/0; //异常抛出点
System.out.println(catchBlock);
return catchBlock;
} finally {
//int a = 5/0; //异常抛出点
System.out.println(finallyBlock);
//return finallyBlock;
}
}
public static void main(String[] args) {
System.out.println(new TryCatchFinally().test());
}
}
3.1 程序正常运行
tryBlock
finallyBlock
tryBlock
3.2 程序异常运行
打开try中注释//int a = 5/0;
tryBlock
catchBlock
finallyBlock
catchBlock
3.2 finally中返回
无论正常运行还是异常运行都返回"finallyBlock"
打开注释 //return finallyBlock;
//正常运行
tryBlock
finallyBlock
finallyBlock
//异常运行
tryBlock
catchBlock
finallyBlock
finallyBlock
3.2 finally中抛出异常
中断finally中语句,对外抛出异常
打开finally中注释//int a = 5/0;
//正常运行
tryBlock
//异常运行
tryBlock
catchBlock
3.2 catch中抛出异常
中止发生异常后中语句,执行finally块中语句,并对外抛出异常
打开catch中注释//int a = 5/0;
打开try中注释//int a = 5/0;
tryBlock
finallyBlock
4 throws声明异常
使用throws声明在方法上声明可能出现的异常。throws是另一种处理异常的方式,它不同于try...catch...finally,throws仅仅是将函数中可能出现的异常向调用者声明,而自己则不具体处理。
throws声明:如果一个方法内部的代码会抛出检查异常(checked exception),而方法自己又没有完全处理掉,则javac保证你必须在方法的签名上使用throws关键字声明这些可能抛出的异常,否则编译不通过。
public void foo() throws ExceptionType1 , ExceptionType2 ,ExceptionTypeN
{
//foo内部可以抛出 ExceptionType1 , ExceptionType2 ,ExceptionTypeN 类的异常,或者他们的子类的异常对象。
}
4 异常类型
在 Java 语言规范中,所有异常都是 Throwable 类或者其子类的实例。Throwable 有两大直接子类。第一个是 Error,涵盖程序不应捕获的异常。第二子类则是 Exception,涵盖程序可能需要捕获并且处理的异常。
imageException 有一个特殊的子类 RuntimeException,用来表示“程序虽然无法继续执行,但是还能抢救一下”的情况。前边提到的数组索引越界便是其中的一种。
RuntimeException 和 Error 属于 Java 里的非检查异常(unchecked exception)。其他异常则属于检查异常(checked exception)。在 Java 语法中,所有的检查异常都需要程序显式地捕获,或者在方法声明中用 throws 关键字标注。通常情况下,程序中自定义的异常应为检查异常,以便最大化利用 Java 编译器的编译时检查。
5 异常的链化
查看Throwable类源码,可以发现里面有一个Throwable字段cause,就是以一个异常对象为参数构造新的异常对象。
public class Throwable implements Serializable {
private Throwable cause = this;
public Throwable(String message, Throwable cause) {
fillInStackTrace();
detailMessage = message;
this.cause = cause;
}
public Throwable(Throwable cause) {
fillInStackTrace();
detailMessage = (cause==null ? null : cause.toString());
this.cause = cause;
}
//........
}
案例
try {
}catch(InputMismatchException immExp){
throw new Exception("计算失败",immExp); /////////////////////////////链化:以一个异常对象为参数构造新的异常对象。
}
java.lang.Exception: 计算失败
at practise.ExceptionTest.add(ExceptionTest.java:53)
at practise.ExceptionTest.main(ExceptionTest.java:18)
Caused by: java.util.InputMismatchException
at java.util.Scanner.throwFor(Scanner.java:864)
at java.util.Scanner.next(Scanner.java:1485)
at java.util.Scanner.nextInt(Scanner.java:2117)
at java.util.Scanner.nextInt(Scanner.java:2076)
at practise.ExceptionTest.getInputNumbers(ExceptionTest.java:30)
at practise.ExceptionTest.add(ExceptionTest.java:48)
... 1 more
6 异常的信息
异常实例的构造十分昂贵。这是由于在构造异常实例时,Java 虚拟机便需要生成该异常的栈轨迹(stack trace)。该操作会逐一访问当前线程的 Java 栈帧,并且记录下各种调试信息,包括栈帧所指向方法的名字,方法所在的类名、文件名,以及在代码中的第几行触发该异常。
image7 java虚拟机中异常处理
程序源文件
public class TryCatchFinally {
private String tryBlock = "tryBlock";
private String catchBlock = "catchBlock";
private String finallyBlock = "finallyBlock";
public String test() {
try {
System.out.println(tryBlock);
//throw new RunTimeException();
return tryBlock;
} catch (Exception e) {
System.out.println(catchBlock);
return catchBlock;
} finally {
System.out.println(finallyBlock);
//return finallyBlock;
}
}
public static void main(String[] args) {
System.out.println(new TryCatchFinally().test());
}
}
程序编译后字节码指令
public java.lang.String test();
descriptor: ()Ljava/lang/String;
flags: ACC_PUBLIC
Code:
stack=2, locals=4, args_size=1
0: getstatic #8 // Field java/lang/System.out:Ljava/io/PrintStream;
3: aload_0
4: getfield #3 // Field tryBlock:Ljava/lang/String;
7: invokevirtual #9 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
10: aload_0
11: getfield #3 // Field tryBlock:Ljava/lang/String;
14: astore_1
15: getstatic #8 // Field java/lang/System.out:Ljava/io/PrintStream;
18: aload_0
19: getfield #7 // Field finallyBlock:Ljava/lang/String;
22: invokevirtual #9 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
25: aload_1
26: areturn
27: astore_1
28: getstatic #8 // Field java/lang/System.out:Ljava/io/PrintStream;
31: aload_0
32: getfield #5 // Field catchBlock:Ljava/lang/String;
35: invokevirtual #9 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
38: aload_0
39: getfield #5 // Field catchBlock:Ljava/lang/String;
42: astore_2
43: getstatic #8 // Field java/lang/System.out:Ljava/io/PrintStream;
46: aload_0
47: getfield #7 // Field finallyBlock:Ljava/lang/String;
50: invokevirtual #9 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
53: aload_2
54: areturn
55: astore_3
56: getstatic #8 // Field java/lang/System.out:Ljava/io/PrintStream;
59: aload_0
60: getfield #7 // Field finallyBlock:Ljava/lang/String;
63: invokevirtual #9 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
66: aload_3
67: athrow
Exception table:
from to target type
0 15 27 Class java/lang/Exception
0 15 55 any
27 43 55 any
7.1 Jvm如何实现异常捕获
-
在编译生成的字节码中,每个方法都附带一个异常表。异常表中的每一个条目代表一个异常处理器,并且由 from 指针、to 指针、target 指针以及所捕获的异常类型构成。这些指针的值是字节码索引(bytecode index,bci),用以定位字节码。
-
其中,from 指针和 to 指针标示了该异常处理器所监控的范围,例如 try 代码块所覆盖的范围。target 指针则指向异常处理器的起始位置,例如 catch 代码块的起始位置。
-
如此程序发生异常JVM会按照顺序遍历异常表中每一个声明的异常,首先从第一行开始,查看触发异常是否是type类指定的异常或其子类,如果是则从target处开始执行。如果不是则继续查看下一条。
Exception table:
from to target type
0 15 30 Class java/lang/Exception
0 15 61 any
30 46 61 any
- 其中Exception类型的异常是我们在应用程序源代码中定义的需要捕获的异常,该异常处理器所监控的范围为字节码指令0~15行。
from to target type
0 15 30 Class java/lang/Exception
//对应Java源文件代码
try {
System.out.println(tryBlock);
return tryBlock;
} catch (Exception e) {
- any类型表示任意类型异常,是JVM负责捕获的异常,当抛出程序没有捕获的异常时由any类型处理。
from to target type
0 15 61 any
30 46 61 any
7.2 程序的正常运行
- 1 如下指令相当于执行try语句中System.out.println(tryBlock);
0: getstatic #8 // Field java/lang/System.out:Ljava/io/PrintStream;
3: aload_0
4: getfield #3 // Field tryBlock:Ljava/lang/String;
7: invokevirtual #9 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
//对应Java源文件代码
try {
System.out.println(tryBlock);
...
}
- 2 如下指令相当于tryBlock变量保存到局部变量表中,待返回时使用
10: aload_0
11: getfield #3 // Field tryBlock:Ljava/lang/String;
14: astore_1
//对应Java源文件代码
try {
...
return tryBlock;
}
- 3 如下指令指令相当于执行finally语句中System.out.println(finallyBlock)。【将finally代码块语句插入到try代码块返回前】
15: getstatic #8 // Field java/lang/System.out:Ljava/io/PrintStream;
18: aload_0
19: getfield #7 // Field finallyBlock:Ljava/lang/String;
22: invokevirtual #9 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
//对应Java源文件代码
finally {
System.out.println(finallyBlock);
}
- 4 加载步骤2中保存的变量返回
25: aload_1
26: areturn
//对应Java源文件代码
try {
...
return tryBlock;
}
7.3 捕获异常处理
按照异常表到发生Exception异常程序会跳到27行指令运行
from to target type
0 15 27 Class java/lang/Exception
- 1 如下指令指令相当于执行catch块语句中System.out.println(catchBlock);,并将异常对象e放入局部变量表的第一个。
27: astore_1
28: getstatic #8 // Field java/lang/System.out:Ljava/io/PrintStream;
31: aload_0
32: getfield #5 // Field catchBlock:Ljava/lang/String;
35: invokevirtual #9 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
//对应Java源文件代码
} catch (Exception e) {
System.out.println(catchBlock);
...
}
- 2 如下指令相当于finallyBlock变量保存到局部变量表中第一个位置,待返回时使用
38: aload_0
39: getfield #5 // Field catchBlock:Ljava/lang/String;
//对应Java源文件代码
} catch (Exception e) {
...
return catchBlock;
}
- 3 如下指令指令相当于执行finally语句中System.out.println(finallyBlock【将finally代码块语句插入到catch代码块返回前】
43: getstatic #8 // Field java/lang/System.out:Ljava/io/PrintStream;
46: aload_0
47: getfield #7 // Field finallyBlock:Ljava/lang/String;
50: invokevirtual #9 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
//对应Java源文件代码
finally {
System.out.println(finallyBlock);
}
- 4 加载步骤2中保存的变量返回
53: aload_2
54: areturn
//对应Java源文件代码
} catch (Exception e) {
...
return catchBlock;
}
7.4 未捕获异常处理
如果try语句发生了catch中未定义的异常,或者catch代码块发生了异常则叫由JVM捕获执行,跳到61行指令运行
from to target type
0 15 61 any
30 46 61 any
- 1 如下指令指令相当于执行finally语句中System.out.println(finallyBlock,同时将异常对象放入局部变量表中。
55: astore_3
56: getstatic #8 // Field java/lang/System.out:Ljava/io/PrintStream;
59: aload_0
60: getfield #7 // Field finallyBlock:Ljava/lang/String;
63: invokevirtual #9 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
- 2 读取步骤1中异常,向方法外抛出。
66: aload_3
67: athrow
7.5 小结
-
可以看出编译器将 finally 代码块的内容,分别放在 try-catch 代码块所有正常执行路径以及异常执行路径的出口中。
-
包含三份 finally 代码块。其中,前两份分别位于 try 代码块和 catch 代码块的正常执行路径出口。最后一份在try 代码块触发的、未被 catch 代码块捕获的异常,以及 catch 代码块触发的异常时执行。
8 Java 7 的 Supressed 异常以及语法糖
- Java 7 引入了 Supressed 异常来解决这个问题。这个新特性允许开发人员将一个异常附于另一个异常之上。因此,抛出的异常可以附带多个异常的信息。然而,Java 层面的 finally 代码块缺少指向所捕获异常的引用,所以这个新特性使用起来非常繁琐。为此,Java 7 专门构造了一个名为 try-with-resources的语法糖,
- 在字节码层面自动使用 Supressed异常。当然,该语法糖的主要目的并不
- 是使用 Supressed异常,而是精简资源打开关闭的用法。
- 在 Java 7 之前,对于打开的资源,我们需要定义一个 finally 代码块,来确保该资源在正常或者异常执行状况下都能关闭。
- 资源的关闭操作本身容易触发异常。因此,如果同时打开多个资源,那么每一个资源都要对应一个独立的 try-finally 代码块,以保证每个资源都能够关闭。这样一来,代码将会变得十分繁琐。
FileInputStream in0 = null;
FileInputStream in1 = null;
FileInputStream in2 = null;
...
try {
in0 = new FileInputStream(new File("in0.txt"));
...
try {
in1 = new FileInputStream(new File("in1.txt"));
...
try {
in2 = new FileInputStream(new File("in2.txt"));
...
} finally {
if (in2 != null) in2.close();
}
} finally {
if (in1 != null) in1.close();
}
} finally {
if (in0 != null) in0.close();
}
Java 7 的 try-with-resources 语法糖,极大地简化了上述代码。程序可以在 try 关键字后声明并实例化实现了 AutoCloseable 接口的类,编译器将自动添加对应的 close() 操作。在声明多个 AutoCloseable 实例的情况下,编译生成的字节码类似于上面手工编写代码的编译结果。与手工代码相比,try-with-resources 还会使用 Supressed 异常的功能,来避免原异常“被消失”。
public class Foo implements AutoCloseable {
private final String name;
public Foo(String name) { this.name = name; }
@Override
public void close() {
throw new RuntimeException(name);
}
public static void main(String[] args) {
try (Foo foo0 = new Foo("Foo0"); // try-with-resources
Foo foo1 = new Foo("Foo1");
Foo foo2 = new Foo("Foo2")) {
throw new RuntimeException("Initial");
}
}
}
// 运行结果:
Exception in thread "main" java.lang.RuntimeException: Initial
at Foo.main(Foo.java:18)
Suppressed: java.lang.RuntimeException: Foo2
at Foo.close(Foo.java:13)
at Foo.main(Foo.java:19)
Suppressed: java.lang.RuntimeException: Foo1
at Foo.close(Foo.java:13)
at Foo.main(Foo.java:19)
Suppressed: java.lang.RuntimeException: Foo0
at Foo.close(Foo.java:13)
at Foo.main(Foo.java:19)
除了 try-with-resources 语法糖之外,Java 7 还支持在同一 catch 代码块中捕获多种异常。实际实现非常简单,生成多个异常表条目即可。
// 在同一 catch 代码块中捕获多种异常
try {
...
} catch (SomeException | OtherException e) {
...
}
网友评论