- Java 的基本理念是 “结构不佳的代码不能运行”
- 错误回复在我们编写的每个程序中都是基本的元素
- Java 的主要目标之一是创建供他人使用的程序构件
- Java 的异常处理的目的在于通过使用少于目前数量的代码来简化大型、可靠的程序的生成,并且通过这种方式可以使你更加自信:你的应用中没有未处理的错误
1. 初识异常
正式介绍异常之前,先来看一个例子:
public static void readFileByBytes(String fileName) {
// 一般先创建file对象
FileInputStream fileInput = null;
try {
File file = new File(fileName);
if (!file.exists()) {
file.createNewFile();
}
byte[] buffer = new byte[1024];
fileInput = new FileInputStream(file);
int byteread = 0;
// byteread表示一次读取到buffers中的数量。
while ((byteread = fileInput.read(buffer)) != -1) {
System.out.write(buffer, 0, byteread);
}
} catch (Exception e) {
// TODO: handle exception
} finally {
try {
if (fileInput != null) {
fileInput.close();
}
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
上述代码展示了一种进行文件读取的方式,是一个运用 Java 异常处理的典型例子。
可能你会有疑问:try、catch、finally 是什么?为什么要写成这个格式?如果不这么写会发生什么情况?
让我们假设这样一个情况:假如在我调用语句 File file = new File(fileName);
创建 File时,由于某种原因未创建成功(比如内存不足,hhh),那么当我后面使用这个不存在的 file 时,就会发生错误,我们的程序会无法继续运行。这种情况下,我们就可以说该程序抛出了一个“异常”,try 就可以理解成“我需要尝试执行一下正常的流程,但是其中会出现某些错误”,之后 Java 会通过 catch 来对这个异常进行"捕获",描述发生某种错误时应该做些什么。
而当无论是否发生了错误,我都需要来做一些清理工作时,就需要使用 finally 语句。finally 中的语句无论如何最终都会执行,这样即便我的代码在中间某部分出错了,但是清理工作依旧可以执行。
上述知识一个基本的介绍,下面我们就一步步学习 Java 中的异常处理机制。
2. 异常的基本常识
2.1 异常和错误
首先需要理解一下关于异常和错误的关系:
- 异常本身只是一个对象,其中包含了错误信息
- 实际上,异常只是我们用来处理错误的手段。我们的代码中可能会出现各种各样的错误,比如使用了一个空引用,比如除数为 0,这些错误会使我们的程序无法继续运行。如果某种情况下,我并不知道如何处理这种错误,那么在这个时候,就需要创建一个代表了错误信息的对象,并将其从当前环境中"抛出"( 抛出异常 ),把错误信息传播到其他环境,表示"这里出现了一个问题,但我无法处理,我把它交给你处理",然后寻找一个恰当的地方来处理这个错误,继续执行程序(否则程序就终止了)。
2.2 异常的抛出
上面我们提到了 抛出异常,下面具体看看抛出异常时会发生哪些事。
- 当程序遇到某些阻止当前方法或作用域继续执行的问题时,且当前环境下无法获得必要的信息来解决问题,于是从当前环境跳出,这问题提交给上一级环境,即抛出异常
- 抛出异常会发生的事:
- 首先使用 new 在堆上创建异常对象
- 然后,当前执行路径被终止,并且从当前环境中弹出对异常对象的引用
- 此时,异常处理机制接管程序,并在异常处理程序中继续执行程序,其任务就是将程序从错误状态中恢复,以使程序要么换一种方式运行,要么继续运行下去。
2.3 Java 标准异常
Java 标准库中内建了一系列的异常,顶级父类为 Throwable,表示任何可以作为异常被抛出的类。
Throwable 可以被分为两种类型:
- Error :用来表示编译时和系统错误,是Java 运行环境的内部错误或者硬件问题,如内存不足等,程序员通常无须关心如何处理(准确来讲是无能为力,除了退出别无他法)。
- Exception:可以被抛出的基本类型,在 Java 类库、用户方法以及运行时故障中都可能抛出 Exception 型异常,是异常处理的核心,我们需要关心的基类型就是 Exception。
图摘自 http://www.cnblogs.com/lulipro/p/7504267.html
声明一点:上面的结构是不完整的,但是在下太懒了,直接接用上述文章的图,感兴趣可以自行查看官方文档进行整理。
实际上,对于 Java 编译时是否对异常进行检查,Exception 又分为两类:
-
不受检查的异常:RuntimeException
-
这种异常属于错误,即便处理也无法使程序恢复,导致的原因通常是因为错误操作(比如使用 null 引用)或者程序员的疏忽(比如数组越界),这部分是由 Java 运行时检测处理,编译器不会检查,因此代码中对该类异常需要忽略。
自动被 Java 虚拟机抛出,自动进行捕获。
-
-
被检查的异常:除了 RuntimeException 及其子类 以外的 Exception 子类
- 程序员处理的实际上就是这部分异常,编译时会被强制检查。
2.4 自定义异常
通常异常的名称代表发生的问题,并且异常的名称应该可以望文知义
如果需要自定义异常,必须继承已有的异常类,最好是选择意思相近的异常类继承,使名字做到望名生义。
按照国际惯例,自定义的异常应该总是包含如下的构造函数:
- 一个无参构造函数
- 一个带有String参数的构造函数,并传递给父类的构造函数。
- 一个带有String参数和Throwable参数,并都传递给父类构造函数
- 一个带有Throwable 参数的构造函数,并传递给父类的构造函数。
下面是IOException类的完整源代码,可以借鉴。
public class IOException extends Exception { static final long serialVersionUID = 7818375828146090155L; public IOException() { super(); } public IOException(String message) { super(message); } public IOException(String message, Throwable cause) { super(message, cause); } public IOException(Throwable cause) { super(cause); } }
2.5 异常的意义
- 异常最重要的方面之一就是如果发生问题,他们将不允许程序沿着其正常的路径继续走下去。一场允许我们强制程序停止运行,并告诉我们出现了什么问题,或者强制程序处理问题,并返回到稳定状态
- 异常代表了当前方法不能继续执行的情形。开发异常处理系统的原因是,如果为每个方法所有可能发生的错误都进行处理的话,任务旧显得过于繁重了,结果常常是将错误忽略。开发的初衷是为了方便程序员处理错误。
- 异常处理的重要原则是“只有在知道如何处理的情况下才捕获异常”,实际上,异常处理的一个重要目标就是把错误处理的代码同错误发生的地点相分离。
3. 异常处理机制
对于异常处理,有两种方式
- try - catch - [finally] 处理
- 函数声明中使用 throws 进行异常说明
如下:
void f() throws 潜在异常列表{//throws 表示可能有一些异常我无法处理,于是向上级抛出
//try-catch-finally 处理当前信息足以解决的异常
try{
//可能抛出异常的方法调用
}catch(SomeException se) {
//必备
//异常处理
}finally{
//可选
//一些清理工作
}
}
一些基础:
- 调用栈:展示了到异常抛出地点的方法调用序列。
3.1 捕获异常
try - catch
try:该块内执行可能产生异常的方法调用
-
catch:即异常处理程序,针对每个要捕获的异常,准备相应的处理程序
catch 必须紧跟 try 之后,异常抛出时,异常处理机制将负责搜寻参数与异常类型相匹配的第一个处理程序。然后进入 catch 子句执行,此时认为异常得到了处理。
一旦 catch 子句结束,则处理程序的查找过程结束。
注意:只有匹配的第一个 catch 子句能执行。
try{ //可能产生异常的代码 }catch(ExceptionType1 id1){ //处理该异常 }catch(ExceptionType1 id2){ //处理该异常 }...
终止模型 & 恢复模型
- Java 支持终止模型:错误非常关键,以至于程序无法回到一场发生的地方继续执行,一旦一场被抛出,就表明错误已无法挽回,也不能回来继续执行
- 恢复模型:异常处理程序的工作室修正错误,然后重新尝试调用出问题的方法,并认为第二次能成功。通常希望亦常被处理之后能继续执行程序。
- Java 可以实现恢复模型:将 try 块放在 while 循环利,这样不断地进入 try 块,知道满意为止;或者遇见错误时不抛出异常,而是调用方法修整
- 为什么 Java 不采用恢复模型:关键在于耦合,恢复性的处理程序需要了解异常抛出地地点,这势必要包含依赖于抛出位置地非通用代码。
重新抛出异常:在 catch 中捕获异常后,得到了对当前异常对象的引用,此时可以直接把它重新抛出。
-
如果只是把当前异常对象重新抛出,那么 printStackTrace() 方法显式地将是原异常抛出点地调用栈信息。
通过调用 fillInStackTrace() 方法可以更新该信息。该方法返回一个 Throwable 对象,它是通过把当前调用栈信息填入原来那个异常对象而建立的。
3.1.2 异常匹配
抛出异常时,会按照代码的书写顺序找最近的处理程序,找到匹配的处理程序之后,就会认为异常将得到处理,然后不再继续查找。
-
查找时,派生类的对象也可以匹配基类的处理程序
因此通常将子类异常放在前面,父类异常放在后面,保证每个 catch 块都有意义
3.1.3 finally 进行清理
用于把除了内存之外的资源恢复到其初始状态。
finally 子句总能执行
- 无论是否捕获异常,finally 总能被执行
- 遇到 return 时,会在return之前执行 finally 里的语句,然后再进行 return
3.2 抛出异常
3.2.1 异常抛出
throw
通过 new 创建异常对象后,引用传递给 throw。
-
throw & return
相似:
- throw 从效果上看就像是从方法“返回”的
- 能用抛出异常的方式从当前的作用域中退出
不同
- 异常返回的地点与普通调用放回的地点完全不同
3.2.2 异常链
异常链:在捕获一个异常后抛出另一个异常,并且把原始异常的的信息保存下来。
- Throwable 的子类在构造器中接收一个 cause 对象作为参数,这个 cause 就用来表示原始异常,这样把原始异常传递给新的异常,使得及时在当前位置创建并抛出了新的异常。也能通过这个异常链追踪到异常最初发生的位置。
- 所有Throwable 的子类只有三种基本的异常类提供了带cause参数的构造器:Error、Exception、以及RuntimrException
- 如果要把其他类型的异常连接起来,那么需要使用initCause方法
3.2.3 异常声明
如果一个方法内部的代码会抛出检查异常(checked exception),而方法自己又没有完全处理掉,则 javac 保证你必须在方法的签名上使用 throws 关键字声明这些可能抛出的异常,否则编译不通过。
它属于方法声明的一部分,紧跟在形参列表之后,使用关键字 throws + 潜在异常类型的列表,仅仅是将函数中可能出现的异常向调用者声明,而自己则不具体处理。
void f() throws 潜在异常列表{
//...
}
3.3 注意事项
覆盖方法的时候,只能抛出在基类方法的异常说明里列出的那些异常。
- 对构造器不起作用,可以抛出任何异常。
派生类构造器的异常说明必须包括基类构造器的异常说明。
派生类构造器不能捕获基类构造器抛出的异常。
对于在构造阶段可能会抛出异常,并且要求清理的类,最安全的使用方式时使用嵌套的 try 语句。
- 基本规则是:在创建需要清理的对象之后,立即进入一个 try-finally 语句
异常处理机制的好处:
- 往往能够降低错误处理代码的复杂度
- 用强制规定的形式来消除错误处理过程中随心所欲的因素
4. 异常使用指南
- 在恰当的级别处理问题(在知道该如何处理的情况下才捕获异常)。
- 解决问题并且重新调用产生异常的方法。
- 进行少许修补,然后绕过异常发生的地方继续执行。
- 用别的数据进行计算,以代替方法预计会返回的值。
- 把当前运行环境下能做的事情尽量做完,然后把相同的异常重抛到更高层。
- 把当前运行环境下能做的事情尽量做完,然后把不同的异常抛到更高层。
- 终止程序。
- 进行简化(如果你的异常模式使问题变得太复杂,那用起来会非常痛苦也很烦人)。
- 让类库和程序更安全(这既是在为调试做短期投资,也是在为程序的健壮性做长期投资)。