Java 异常机制总结
前言
本文是对《Java核心技术卷Ⅰ》(第十版)第七章异常部分(264-285)的学习总结,目的是记录自己的学习路径,以及方便日后查看、复习、补充。
Java异常机制
与大多数编程语言一样,Java提供了异常处理(Exception Handing)机制,这种机制能够将程序中可能出现的错误进行捕获并进行对应的处理,以增强程序的健壮性。
异常层次结构
在Java 程序设计语言中,异常对象都是派生于Throwable类 的一个实例,所有的异常都是由Throwable类继承而来。Throwable类具有两个子类:Error类和Exception类 。
Error类 描述了Java运行时系统内部错误和资源耗尽错误 。应用程序不应该抛出这种类型的异常对象,我们也无法处理该类异常。Exception类 分为两个分支:RuntimeException类 和 IOException类 其中,由程序本身错误造成的异常属于RuntimeException异常,而程序本身没有问题,但是由于诸如I/O错误这类问题导致的异常属于其他异常。
原则上讲,异常处理机制关注的是RuntimeException异常,该类异常往往由程序本身造成,程序员需要对自己所写的bug负责 。
受查异常
Java语言规范将派生于Error类和RuntimeException类的所有异常称为非受查(unchecked)异常,而其他所有异常称之为受查(checked)异常。这个概念十分重要,简单粗暴的理解,受查异常即需要手动去抛出的异常,一般指由程序外部因素造成的异常(如空文件异常),程序语言本身无法控制外部因素,不确定是否会发生异常,所以要手动去检查异常。
throws语句
声明受查异常
方法应该在首部声明所有有可能抛出的异常,这样可以从首部反映出这个方法可能抛出哪类受查异常,以便于调用该方法的人做进一步的处理 。Java提供了throws语句声明异常,语法格式如下——
public FileInputStream(String name) throws FileNotFoundException
通常遇到以下4种情况应该使用throws抛出异常:
1. 调用抛出受查异常的方法,如方法中调用了FileInputStream;
2. 程序运行过程发现错误,并利用throws抛出异常,这种情况常与try-catch结合使用;
3. 程序本身错误,如数组越界,这种情况不常用,低级错误应当在主观上避免;
4. Java虚拟机和运行库出现的内部错误,这种情况也不常用,这是难以抗拒的异常。
抛出异常的使用形式:
class MyAnimation{
...
public Image loadImage(String s) throws IOException,FileNotFoundException{
// code
}
多个异常使用逗号隔开。
总之,一个方法必须声明所有可能抛出的受查异常,而非受查异常,要么不可控(Error),要么需要在debug层面避免(RuntimeException)。特别的,如果在子类中覆盖了超类的一个方法,子类方法中声明的受查异常不能比父类声明的异常更通用。
throws 抛出异常
throws除了可以在方法首部声明所抛出异常之外,还可以在程序任意位置抛出异常,具体使用方式形如——
if(触发IO异常判定条件)
throws new IOException();
一旦抛出异常,throws所在的方法不可能返回到调用者,该方法此刻结束。
捕获异常
try-catch块
与大多数编程语言一样,Java提供了try-catch捕获异常的方法,语法如下:
try
{
//code
}
catch(Exception e)
{
// handle exception
}
如果在try语句块中没有抛出异常,try语句块全部代码将会执行,catch语句块不执行;
如果在try语句块中出现异常,并被对应的catch语句中的异常类型匹配,那么该异常将被捕获:try语句块中抛出异常后的代码不被执行,捕获异常的catch语句块执行;
如果出现异常,且不与任何catch说明的异常匹配,那么该方法立刻退出。
多个catch语句块可以合并,但此时异常变量e隐含为final。
try
{
//code
}
catch(FileNotFoundException|IOException e)
{
// handle exception
}
上文中提到过,子类覆盖父类方法,不能使用throws抛出更特化的异常,但是如果父类方法没有声明任何异常,而子类的方法中存在异常,此时throws语句无法使用,则可以使用try-catch去捕获异常。实际上,throws与try-catch组合使用效果更佳,try-catch捕获异常作为异常原因,然后在catch语句中使用throws抛出新的异常,来进一步解释该异常形成的原因,比如:
try
{
// access database
}
catch(SQLException e)
{
Throwable se = new ServletException("database error");
se.initCause(e);
throw se;
}
此时,抛出的se异常对象具有更详细的信息和异常类型。
finally语句
try-catch在捕获异常之后,通常会跳过一些代码块,甚至直接退出方法,这个时候,就需要finally语句来做一些“善后工作”。Java提供了finally语句,无论是否有异常发生,该语句都会被执行:
try{
//1
//code that might throw exceptions
//2
}
catch(Exception e)
{
//3
//handle exception
//4
}
finally
{
//5
}
//6
如果代码没有异常,先执行try语句块,然后执行finally,执行顺序:1->2->5->6
如果异常被catch捕获,先执行try语句块异常之前的代码,然后catch,最后finally,执行顺序:1->3->4->5->6
如果异常被catch捕获,且catch抛出新的异常,那么执行顺序变成:1->3->5
如果出现catch无法捕获的异常,那么先执行try异常之前的代码,catch块跳过,finally块执行,然后方法退出,执行顺序变成:1->5
特别的,如果finally有return语句,那么会覆盖之前方法中所有的return语句。
带资源的try语句
try抛出异常后,很多代码块会被跳过,导致一些资源没有及时回收。finally机制提供了一个回收资源的机会,但是同时也带来一个问题,如果finally语句块中也抛出异常,那么会覆盖try语句中的异常,那么我们原本真正想要处理的异常就无法正确的返回到调用者了。因此,Java7 提供了try资源处理机制,可以把资源回收的问题交给try,语法格式如下:
try(Scanner in = new Scanner(System.in))
{
// exception
}
当try正常退出时,in打开的输入流会自动关闭。
分析堆栈轨迹元素
堆栈轨迹(Stack trace)是一个方法调用过程的列表,包含了程序执行过程中的方法调用信息,如类名、方法名和所在代码段行号等。可以通过Throwable
类的printStackTrace
方法访问堆栈轨迹文本描述信息,也可以通过getStackTrace
方法获取StackTraceElement
对象数组,获取文件名、行号和类名、方法名等信息,演示程序如下:
import java.util.Scanner;
public class StackTrace {
public static int factorial(int n){
System.out.println("factorial("+n+"):");
Throwable t = new Throwable();
StackTraceElement[] stackTraceElements = t.getStackTrace();
for(StackTraceElement elem:stackTraceElements){
System.out.println(elem);
}
int r;
if(n<=1) r=1;
else r = n*factorial(n-1);
System.out.println("return "+r);
return r;
}
public static void main(String[] args){
Scanner in = new Scanner(System.in);
factorial(in.nextInt());
}
}
有些时候编译器不提供这些信息的时候,可以手动输出,有助于debug。