捕获和抛出异常
# 捕获和抛出异常
异常处理模型基于三种操作:声明一个异常(declaring an exception)、抛出一个异常 (throwing an exception)和捕获一个异常(catching an exception)。
# 概述
我们来简单说明下上述三种操作
method2() throws Exception{
if (an error occurs){
throw new Exception();
}
}
2
3
4
5
在 method2 的方法头里,声明了它可能抛出的必检异常的类型,这里简单写了 Exception
,具体情况具体分析,如果是操作 IO,那么应该抛出 IOException
。
在第 3 行抛出了一个异常。
在 method1 里用了 try-catch 块来捕获异常:
method1(){
try{
method2();
}catch(Exception ex){
process exception;
}
}
2
3
4
5
6
7
# 声明异常
在 Java 中,当前执行的语句必属于某个方法。一个程序从 main 方法开始执行一个程序。每个方法都必须声明它可能抛出的必检异常的类型,这称为声明异常( declaring exception)。
由于任何代码都可能发生免检异常,因此无需在方法里显示声明;但必检异常必须在方法头中显式声明,这样,方法的调用者会被告知有异常。
为了在方法中声明一个异常,就要在方法头中使用关键字 throws:
public void myMethod() throws IOException
关键字 throws 表明 myMethod 方法可能会抛出异常 IOException。如果方法可能会抛出多个异常,就可以在关键字 throws 后添加一个用逗号分隔的异常列表:
public void myMethod() throws Exceptionl, Exception2, …, ExceptionN
注意:如果方法没有在父类中声明异常,那么就不能在子类中对其进行继承来声明异常,例如下述代码:
public class LearnExceptionDemo1 {
public static void main(String[] args) {
try {
new Student().run();
} catch (Exception e) {
}
}
}
class Person{
void run(){
System.out.println("person run!");
}
}
class Student extends Person{
@Override
void run() throws Exception {
System.out.println("student run!");
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
编译会报错:
$ javac LearnExceptionDemo1.java
LearnExceptionDemo1.java:22: 错误: Student中的run()无法覆盖Person中的run()
void run() throws Exception {
^
被覆盖的方法未抛出Exception
1 个错误
2
3
4
5
6
# 抛出异常
检测到错误的程序可以创建一个合适的异常类型的实例并抛出它,这就称为抛出一个异常(throwing an exception)。
例如,假如程序发现传递给方法的参数与方法的合约不符(例如,方法中的参数必须是非负的,但是传入的是一个负参数),这个程序就可以创建 IllegalArgumentException
的一个实例:
IllegalArgumentException ex = new II1egalArgumentException("Wrong Argument");
tips:Java 中每个异常类至少有两个构造方法:一个无参构造方法和一个带有描述这个异常的 String 参数的构造方法。该参数称为异常消息(exception message),它可以用
getMessage()
获取。
然后可以抛出(throw)该异常:
throw ex;
或者简化为一条语句:
throw new IllegalArgumentException ("Wrong Argument");
# 捕获异常
当抛出一个异常时,可以在 try-catch 代码块中捕获和处理它,格式如下:
try {
statements; // Statements that may throw exceptions
}
catch (Exception1 exVarl){
handler for exceptionl;
}
catch (Exception2 exVar2){
handler for exception2;
}
...
catch (ExceptionN exVarN){
handler for except!onN;
}
2
3
4
5
6
7
8
9
10
11
12
13
首先,我们将可能发生异常的代码,用 try 块包住,然后会发生两种情况:一种是发生了异常,另一种没有发生。我们来分别看看两种情景,try-catch 代码块的处理逻辑
# 如果发生了异常
我们运行代码,如果发生了异常,则:
- 跳过 try 块中剩余的代码,然后开始查找处理这个异常的代码块,也就是 catch 块。
- 每个 catch 处理一种异常,例如
IOExcetion
,NullPointerException
,每个 catch 块可以称为一个异常处理器(exception handler)。 - 查找的过程是从上往下,例如判断抛出的异常是否 Exception1,是则由第一个 catch 块处理,并且该异常会赋值给 exvarl 参数;如果不是,则继续看是抛出的异常是否 Exception2;以此类推。
- 对应的 catch 块处理完后,则执行 try-catch 块之后的代码。多个
catch
语句只有一个能被执行 - 如果没有发现异常处理器,Java 会退出这个方法,把异常抛给调用这个方法的方法,然后用同样的过程来查找处理器;
- 如果在调用方法链中找不到异常处理器,Java 程序会终止。
# 捕获异常举例
假设 main 方法调用 method1, method1 调用 method2,method2 调用 methods3, method3 抛出一个异常,如图 所示。
我们分几种情况来看,异常的捕获是怎么样的:
- 首先发生了异常,try 块中剩下的 statement5 代码就不会再被执行了
- 如果异常类型是 Exceprion3,它就会被 method2 中处理异常 ex3 的 catch 块捕获并处理。既然异常已经被处理了,那么程序可以继续执行其他代码了,然后执行 statement6 处的代码。
- 如果异常类型是 Exception2,则退出 method2,控制被返回给 method1,然后跳过 statement3,异常就会被 method1 中处理异常 ex2 的 catch 块捕获,然后执行 statement4
- 如果异常类型是 Exception1, 则退出 method1, 控制被返回给 main 方法,跳过 statement1,异常会被 main 方法中处理异常 ex1 的 catch 块捕获。然后执行 statement2
- 如果异常类型没有在 method2、method1 和 main 方法中被捕获,程序就会终止。不执行 statement1 和 statement2
# 如果没有发生异常
如果在执行 try 块的过程中没有出现异常,则跳过 catch 子句,也就是继续执行 try-catch 块之后的代码。
# 注意点
- 一个通用的异常父类有很多的子类,如果使用 catch 块捕获父类,那么只要父类和其子类的异常对象都满足被捕获的条件(就好比用 instanceof 父类 )
- 注意父类的 catch 块,要出现在子类的 catch 块之前,否则会编译报错。例如
RuntimeException
是Exception
的子类,必须先捕获RuntimeException
,再捕获Exception
。这很好理解,如果父类现在前面,那么子类的异常是永远捕获不到的 - 如果一个方法声明了一个必检异常,那么必须在 try-catch 块中调用它,或者在调用的方法中声明异常。例如,InputStream 类的 read 方法就声明了异常 IOException,因此几乎所有调用 IO 的方法都要声明异常。
# 捕获多种异常
Java7 版本出了一个新特性,可以在一个 catch 块中捕获多种类型的异常:每个异常类型使用竖线( | )与下一个分隔。如果其中一个异常被捕获,则执行处理的代码。
catch (Exceptionl | Exception2 | … | Exceptionk ex){
}
2
3
这样消除了重复的代码,例如下面的例子中,每个 catch 块都有重复的代码:
catch (IOException ex) {
logger.log(ex);
throw ex;
catch (SQLException ex) {
logger.log(ex);
throw ex;
}
2
3
4
5
6
7
可以简写为:
catch (IOException|SQLException ex) {
logger.log(ex);
throw ex;
}
2
3
4
注意:
- 如果 catch 块处理多种异常类型,那么 catch 参数隐式的声明为 final,因此在 catch 块中不能给它指定任意的值
- 编译一个处理多种异常类型的 catch 块产生的字节码,比编译多个 catch 块但每个只能处理一种异常类型产生的字节码要少(因此优越一些)。在由编译器生成的字节码中,一个处理多种异常类型的 catch 块没有重复; 字节码没有复制异常处理器。
# 小结
异常处理模型基于三种操作:声明一个异常、抛出一个异常和捕获一个异常
声明异常的关楗字是 throws, 抛出异常的关键字是 throw。
异常的处理器是通过从当前的方法开始,沿着方法调用链,按照异常的反向传播方向找到的。