1、异常分类
所有的异常对象都是由Throwable
继承而来。
在下一层中分解为两个分支:Error
和Exception
。
Error
类层次结构描述了程序无法处理的错误,如Java运行时系统的内部错误和资源耗尽错误。大多数错误与代码编写者的操作无关,而是代码运行时JVM出现的问题。这些异常发生时,JVM一般会选择线程终止。这些错误是不可查的,因为它们在应用程序的控制和处理能力之外,而且绝大多数是程序运行时不允许出现的状况。对于设计合理的应用程序来说,即使确实发生了错误,本质上也不应该试图去处理它所引起的异常状况。(Error由JVM产生并抛出,程序不需进行处理。
)
Exception
层次结构又分解为两个分支:一个派生于RuntimeException,另一个包含其他异常。划分这两个分支的规则是:由程序逻辑错误导致的异常属于RuntimeException,而程序本身没有问题,但由于像I/O错误这类问题导致的异常属于其他异常。
(如果出现RuntimeException异常,那么就一定是你的问题!)
派生于RuntimeException的异常示例
:错误的类型转换、数组访问越界、访问null指针不是派生于RuntimeException的异常示例
:试图在文件尾部后面读取数据、试图打开不存在的文件、试图根据给定的字符串查找Class对象,而该字符串表示的类并不存在。
Java语言规范将派生于Error类或RuntimeException类的所有异常称为不可查(unchecked)异常
,所有其他的异常称为可查(checked)异常
。
- 对于
可查异常
,编译器要求必须处置,也就是说当程序中可能出现这类异常,要么用try-catch
语句捕获它,要么用throws
子句声明抛出它,否则编译不会通过。编译器将核查是否为所有的受查异常提供了异常处理器。 - 对于
不可查异常
,编译器不要求强制进行处置,它们要么不可控制(Error),要么应该避免发生(RuntimeException)
。
自定义异常:
当我们在代码中需要抛出异常时,尽量使用JDK已定义的异常类型。例如,参数检查不合法,应该抛出IllegalArgumentException
。
在一个大型项目中,可以自定义新的异常类型,但是,保持一个合理的异常继承体系
是非常重要的。
一个常见的做法是自定义一个BaseException
作为“根异常
”,然后,派生出各种业务类型的异常
。
BaseException
需要从一个适合的Exception
派生,通常建议从RuntimeException
派生:
public class BaseException extends RuntimeException {
}
其他业务类型的异常就可以从BaseException
派生:
public class UserNotFoundException extends BaseException {
}
public class LoginFailedException extends BaseException {
}
...
自定义的BaseException
应该提供多个构造方法:
public class BaseException extends RuntimeException {
public BaseException() {
super();
}
public BaseException(String message, Throwable cause) {
super(message, cause);
}
public BaseException(String message) {
super(message);
}
public BaseException(Throwable cause) {
super(cause);
}
}
上述构造方法实际上都是原样照抄RuntimeException
。这样,抛出异常的时候,就可以选择合适的构造方法。通过IDE可以根据父类快速生成子类的构造方法。
2、异常处理
2.1 异常声明—throws
如果一个方法可能会出现异常,但没有能力处理这种异常,可以在方法声明处用throws子句来声明抛出异常。
throws语句用在方法定义时声明该方法要抛出的异常类型:
methodname throws Exception1,Exception2,..,ExceptionN { }
方法名后的throws Exception1,Exception2,…,ExceptionN 为声明要抛出的异常列表。当方法抛出异常列表的异常时,方法将不对这些类型及其子类类型的异常作处理,而抛向调用该方法的方法,由它去处理。
使用throws关键字将异常抛给调用者后,如果调用者不想处理该异常,可以继续向上抛出
,但最终要有能够处理该异常的调用者。
异常抛出的规则:
- 如果是
不可查异常
,那么可以不使用throws关键字来声明要抛出的异常,编译仍能顺利通过,但在运行时会被系统抛出。 - 如果一个方法可能出现
可查异常
,要么用try-catch语句捕获,要么用throws子句声明将它抛出,否则会导致编译错误。 - 只有当抛出了异常时,该方法的调用者才必须处理或者重新抛出该异常。当方法的调用者无力处理该异常的时候,应该继续抛出。
方法覆盖时异常抛出规则
:
1)如果在子类中覆盖了超类的一个方法,子类方法中声明的受查异常不能比超类方法中声明的异常更通用。(也就是,子类方法中可以抛出更特定的异常,或者根本不抛出任何异常。)
2)如果超类方法没有抛出任何受查异常,子类也不能抛出任何受查异常。
3)如果类中的一个方法声明将会抛出一个异常,而这个异常是某个特定类的实例时,则这个方法就有可能抛出一个这个类的异常,或者这个类的任意一个子类的异常。
2.2 异常抛出—throw
throw总是出现在方法体
中,用来抛出一个Throwable类型的异常。程序会在throw语句后立即终止,它后面的语句执行不到,
然后在包含它的所有try块中(可能在上层调用函数中)从里向外寻找含有与其匹配的catch子句的try块。
如果所有方法都层层上抛获取的异常,最终JVM会进行处理,处理也很简单,就是打印异常消息和堆栈信息。
对于一个异常对象,真正有用的信息是异常的对象类型,而异常对象本身毫无意义。比如一个异常对象的类型是ClassCastException,那么这个类名就是唯一有用的信息。所以,在选择抛出什么异常时,最关键的就是选择异常的类名能够明确说明异常情况的类。
异常对象通常有两种构造函数:一种是无参数的构造函数;另一种是带一个字符串的构造函数,这个字符串将作为这个异常对象除了类型名以外的额外说明。
2.3 异常捕获—try/catch
2.3.1 异常捕获
在Java中,异常通过try-catch语句捕获。其一般语法形式为:
try {
// 可能会发生异常的程序代码
} catch (Type1 id1){
// 捕获并处置try抛出的异常类型Type1
} catch (Type2 id2){
//捕获并处置try抛出的异常类型Type2
}
关键词try后的一对大括号将一块可能发生异常的代码包起来,称为监控区域
。
Java方法在运行过程中出现异常,则创建异常对象。将异常抛出监控区域之外,由Java运行时系统试图寻找匹配的catch子句以捕获异常。若有匹配的catch子句,则运行其异常处理代码,try-catch语句结束。
匹配的原则是
:如果抛出的异常对象属于catch子句的异常类,或者属于该异常类的子类,则认为生成的异常对象与catch块捕获的异常类型相匹配。
注意:一旦某个catch捕获到匹配的异常类型,将进入异常处理代码。一经处理结束,就意味着整个try-catch语句结束。其他的catch子句不再有匹配和捕获异常类型的机会
。
Java通过异常类描述异常类型,对于有多个catch子句的异常程序而言,应该尽量将捕获底层异常类的catch子句放在前面,同时尽量将捕获相对高层的异常类的catch子句放在后面。否则,捕获底层异常类的catch子句将可能会被屏蔽。
例如:RuntimeException异常类包括运行时各种常见的异常,ArithmeticException类和ArrayIndexOutOfBoundsException类都是它的子类。因此,RuntimeException异常类的catch子句应该放在 最后面,否则可能会屏蔽其后的特定异常处理或引起编译错误。
如果某个异常发生的时候没有在任何地方进行捕获,那程序就会终止执行,并在控制台上打印出异常信息,其中包括异常的类型和堆栈的内容。
一段读取数据的典型程序代码:
public void read(String filename) {
try {
InputStream in = new FileInputStream(filename);
int b;
while((b = in.read()) != -1) {
//process input
}
}
catch (IOException exception) {
exception.printStackTrace();
}
}
除在程序中捕获异常并进行处理外,通常更好的选择是什么都不做,而是将异常传递给调用者。如果采取这种处理方式,就必须声明这个方法可能会抛出一个IOException。
public void read(String filename) throws IOException {
InputStream in = new FileInputStream(filename);
int b;
while((b = in.read()) != -1) {
//process input
}
}
注意:编译器将严格执行throws说明符。如果调用了一个抛出受查异常的方法,就必须对它进行处理,或者继续传递
。
通常,应该捕获那些知道如何处理的异常,而将那些不知道怎么处理的异常继续进行传递。但该规则也有一个例外:前面提到过,如果编写一个覆盖超类的方法,而这个方法又没有抛出异常,那么这个方法就必须捕获方法代码中出现的每个受查异常。
2.3.2 捕获多个异常
在Java SE 7中,同一个catch子句中可以捕获多个异常类型。
例如对应缺少文件和未知主机异常的动作是一样的,就可以合并catch子句:
try {
//code that might throw exceptions
}
catch (FileNotFoundException | UnknownHostException e) {
//emergency code 1
}
catch (IOException e) {
//emergency code 2
}
只有当捕获的异常类型彼此之间不存在子类关系时才需要这个特性。另外,当捕获多个异常时,异常变量隐含为final变量。例如不能在上面code 1中为e赋不同的值。
2.3.3 再次抛出异常与异常链
在catch子句中可以抛出一个异常,目的是改变异常的类型。如果开发了一个供其他程序员使用的子系统,那么用于表示子系统故障的异常类型可能会产生多种解释。ServletException就是这样一个异常类型的例子。执行Servlet的代码可能不想知道发生错误的细节原因,但希望明确知道servlet是否有问题。
try {
//access the data
}
catch (SQLException e){
throw new ServletException("database error: "+e.getMessage());
}
这里ServletException用带有异常信息文本的构造器来构造。不过还有一种更好的处理方式,并且将原始异常设置为新异常的“原因”:
try {
//access the data
}
catch (SQLException e){
Throwable se = new ServletException("database error: ");
se.initCause(e);
throw se;
}
当捕获到异常时,可使用Throwable e = se.getCause();
得到原始异常。强烈使用这种包装技术,可以让用户抛出子系统中的高级异常,而不会丢失原始异常的细节。
2.3.4 finally子句
利用finally子句对已分配资源回收或关闭数据库连接
InputStream in = new FileInputStream(…);
try {
//1
code
//2
}
catch {
//3
show error message
//4
}
finally {
//5
in.close();
}
//6
几种情况:
- 代码未发生异常:1、2、5、6
- 抛出一个在catch子句中捕获的异常:
- catch子句中没有抛出异常:1、3、4、5、6
- catch子句中抛出异常:1、3、5
- 代码抛出一个异常,但该异常不是由catch子句捕获的:1、5
注意:
1、try子句可以只有finally子句,而没有catch子句。
2、当try-catch-finally中包含return语句时,程序的执行和返回情况:
JavaSE之彻底搞懂try,catch,finally与return的执行
谈谈Java中try-catch-finally中的return语句
JAVA异常及其异常处理方式
3、如果在finally块中抛出异常,try块捕捉的异常就不能抛出,外部捕捉到的异常就是finally块中的异常信息,而try块中发生的真正的异常堆栈信息则丢失了。
4、在下述4种特殊情况时,finally块都不会被执行:
1)在finally语句块中发生了异常。
2)在前面的代码中用了System.exit()退出程序。
3)程序所在的线程死亡。
4)关闭CPU。
2.3.5 带资源的try语句
对通过以下代码模式关闭资源:
open a resource
try {
work with the resource
}
finally {
close the resource
}
存在两种问题:(1)略繁琐(2)假设在try语句块中抛出了一些非IOException的异常,这些异常只有该方法的调用者才能处理。同时,调用close方法有可能抛出IOException异常。当出现这种情况时,原始的异常将会丢失,转而抛出close方法的异常,而这经常并不是我们想要的。
假设资源属于一个实现了AutoCloseable接口的类,Java SE 7为这种代码模式提供了一个很有用的快捷方式。AutoCloseable接口有一个方法:
void close() throws Exception
带资源的try语句的最简形式为:
try (Resource res = …)
{
work with res
}
try块退出时,会自动调用res.close()。给出一个典型的例子,读取一个文件中的所有单词:
try (Scanner in = new Scanner(new FileInputStream("/usr/share/dict/words"),"UTF-8"))
{
while(in.hasNext())
System.out.println(in.next());
}
这个块正常退出时,或者存在一个异常时,都会调用in.close()方法,就好像使用了finally块一样。
还可以指定多个资源:
try (Scanner in = new Scanner(new FileInputStream("/usr/share/dict/words"),"UTF-8");
PrintWriter out = new PrintWriter("out.txt"))
{
while (in.hasNext())
out.println(in.next().toUpperCase());
}
与此同时,带资源的try语句可以很好的处理close语句抛出的异常覆盖try语句抛出的异常这种情形。使用带资源的try语句,原来的异常会重新抛出,而close方法抛出的异常会“被抑制”。这些异常将自动捕获,并由addSuppressed方法增加到原来的异常。如果对这些异常感兴趣,可调用getSuppressed方法,它会得到从close方法抛出并被抑制的异常列表 。
2.3.6 分析堆栈轨迹元素
见《Java核心技术 卷I》P280