1. Java异常的分类和类结构图
Java标准库内建了一些通用的异常,这些类以Throwable为顶层父类。
Throwable又派生出Error类和Exception类。
错误:Error类以及他的子类的实例,代表了JVM本身的错误。错误不能被程序员通过代码处理,Error很少出现。因此,程序员应该关注Exception为父类的分支下的各种异常类。
异常:Exception以及他的子类,代表程序运行时发送的各种不期望发生的事件。可以被Java异常处理机制使用,是异常处理的核心。
总体上我们根据Javac对异常的处理要求,将异常类分为2类。
非检查异常(unckecked exception): Error 和 RuntimeException 以及他们的子类。 javac在编译时,不会提示和发现这样的异常,不要求在程序处理这些异常。所以如果愿意,我们可以编写代码处理(使用try…catch…finally)这样的异常,也可以不处理。对于这些异常,我们应该修正代码,而不是去通过异常处理器处理 。这样的异常发生的原因多半是代码写的有问题。如除0错误ArithmeticException,错误的强制类型转换错误ClassCastException,数组索引越界ArrayIndexOutOfBoundsException,使用了空对象NullPointerException等等。
检查异常(checked exception):除了Error 和 RuntimeException的其它异常。 javac强制要求程序员为这样的异常做预备处理工作(使用try…catch…finally或者throws)。在方法中要么用try-catch语句捕获它并处理,要么用throws子句声明抛出它,否则编译不会通过。这样的异常一般是由程序的运行环境导致的。因为程序可能被运行在各种未知的环境下,而程序员无法干预用户如何使用他编写的程序,于是程序员就应该为这样的异常时刻准备着。如SQLException , IOException,ClassNotFoundException 等。
2. 自定义异常类
如果要自定义异常类,则扩展Exception类即可,因此这样的自定义异常都属于检查异常(checked exception)。如果要自定义非检查异常,则扩展自RuntimeException。
按照国际惯例,自定义的异常应该总是包含如下的构造函数:
- 一个无参构造函数
- 一个带有String参数的构造函数,并传递给父类的构造函数。
- 一个带有String参数和Throwable参数,并都传递给父类构造函数
- 一个带有Throwable 参数的构造函数,并传递给父类的构造函数。
代码示例
1 | public class IOException extends Exception |
3. 异常的抛出
throw:
- throw 语句用在方法体内,表示抛出异常,由方法体内的语句处理。
- throw 是具体向外抛出异常的动作,所以它抛出的是一个异常实例,执行 throw 一定是抛出了某种异常。
throws:
- throws 语句是用在方法声明后面,表示如果抛出异常,由该方法的调用者来进行异常的处理。
- throws 主要是声明这个方法会抛出某种类型的异常,让它的使用者要知道需要捕获的异常的类型。
- throws 表示出现异常的一种可能性,并不一定会发生这种异常
实例
1 | String readData(Scanner in) throws EOFException { |
注意事项:
- 子类在覆盖父类方法时,父类的方法如果抛出了异常,那么子类的方法只能抛出父类方法的异常或该异常的子类;
- 如果父类抛出多个异常,那么子类只能抛出父类异常的子集。简单说就是子类覆盖父类的方法只能抛出父类的异常或子类或子集。
- 如果父类的方法没有抛出异常,那么子类覆盖时绝对不能抛出异常而只能捕获。
3.1 再次抛出异常
在catch子句中可以抛出一个异常,这样做的目的是改变是异常的类型。
普通的方式:
1
2
3public void demo() throws Exception {
throw MyException();
}使用异常封装的方式(建议):
1
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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45package exception;
public class MyDemo {
private static Throwable se = null;
public MyDemo() {
}
public void count() throws Throwable {
try {
int i = 10/0;
} catch (Exception ex) {
se = new MyException("除0异常信息封装!");
se.initCause(ex);
throw se;
}
}
public static void main(String[] args) {
try {
MyDemo mydemo = new MyDemo();
mydemo.count();
} catch (Throwable ex) {
//在这里可以获取原始异常,即count方法里产生的除0异常
Throwable e = se.getCause();
System.out.println(e.getClass().getName());
}
}
/**
* 自定义异常类
*/
class MyException extends Exception {
public MyException() {
super();
}
public MyException(String message) {
super(message);
}
}
}
输出结果:
1 | java.lang.ArithmeticException |
使用异常包装技术,这样可以让用户抛出子系统中的高级异常而不会丢失原始异常的细节。
如果在一个方法中发生了一个受查异常,而不允许抛出它,那么包装技术就十分有用。我们可以捕获这个受查异常,并将它包装成一个运行时异常。
4. 异常捕获
使用 try 和 catch 关键字可以捕获异常。try/catch 代码块放在异常可能发生的地方。
try/catch代码块中的代码称为保护代码,使用 try/catch 的语法如下:
1 | try |
4.1 多重捕获
一个 try 代码块后面跟随多个 catch 代码块的情况就叫多重捕获。
多重捕获块的语法如下所示:
1 | try{ |
上面的代码段包含了 3 个 catch块。
可以在 try 语句后面添加任意数量的 catch 块。
如果保护代码中发生异常,异常被抛给第一个 catch 块。
如果抛出异常的数据类型与 ExceptionType1 匹配,它在这里就会被捕获。
如果不匹配,它会被传递给第二个 catch 块。
如此,直到异常被捕获或者通过所有的 catch 块。
注意: 多catch块时,捕获父类异常的catch放在最下面。
4.2 多重捕获的另一种形式
在Java SE 7 中,同一个catch子句中可以捕获多个异常类型。例如,假设对应缺少文件和未知主机异常的动作是一样的,就可以合并cacth子句:
1 | try { |
注意:只有当捕获的异常类型彼此之间不存在子类关系时才需要这个特性,即才能合并多catch块。
5. finally子句
finally 关键字用来创建在 try 代码块后面执行的代码块。
无论是否发生异常,finally 代码块中的代码总会被执行。
通常用来关闭(释放)资源,如流的关闭等。
finally 代码块出现在 catch 代码块最后,语法如下:
1 | try{ |
提示:建议使用解耦合try/catch和try/finally语句块。这样可以提高代码的清晰度。代码示例:
1 | InputStream in = ...; |
内层的try语句块只有一个职责,就是确保关闭输入流。外层的try语句块也只有一个职责功能,就是确保报告出现的错误。这种设计方式不仅清楚,而且还具有一个功能,就是将会报告finally子句中出现的错误。
5.1 finally子句包含return语句
示例:
1 | try { |
在执行上述代码时,try语句会发生错误从而执行catch块中的语句。在执行retrun 2;这条语句时,发现有finally语句块所以从而去执行finally语句块中的内容,执行了return 3;后该方法就结束了。如果finally语句中不是return语句而是其他逻辑操作,则在执行完finally语句中的内容之后会回来返回执行catch中的内容,即返回2。
6. 带资源的try语句
假设有这种情况的发生:假设在try语句块中代码抛出了一些非IOException异常,这些异常只有这个方法的调用者才能够给予处理。执行finally语句块,并调用close方法。而close方法本身也有可能抛出IOException异常。当这种情况发生时,原始的异常将会丢失,转而抛出close方法的异常。
普通解决方法代码示例:
1 | InputStream in = ...; |
这种方法会使代码变得极其繁琐,我们使用Java SE 7 的带资源try语句去处理。它允许在try关键字后跟一对圆括号,圆括号可以声明,初始化一个或多个资源,此处的资源指得是那些必须在程序结束时必须关闭的资源(比如数据库连接,网络连接等),try语句在该语句结束时自动关闭这些资源。
为了保证try语句可以正常关闭资源,这些资源实现类必须实现Closeable或AutoCloseable接口,实现这些类就必须实现close方法(一般情况不用自己实现)。
代码示例:
1 | try (InputStream in = ...; PrintWriter out = ...) { |