Java的基本理念是“结构不佳的代码不能运行”。
基本异常
异常情形(exceptional condition)是指阻止当前方法或作用域继续执行的问题。把异常情形与普通问题区分很重要,所谓的普通问题是指,在当前环境下能得到足够的信息,总能处理这个错误。对于异常情形就不能继续下去了,因为在当前环境下无法获得必要信息解决问题。你只能做到从当前环境跳出,并把问题交给上一级环境。这就是抛出异常时所发生的事情。
抛出异常后,有几件事会随之发生。首先,同Java中其他对象的创建一样,将使用new在堆上创建异常对象。然后当前的执行路径被终止,并从当前环境中弹出对异常对象的引用。此时,异常处理机制接管程序,并开始寻找一个恰当的地方来执行程序。这个恰当的地方就是异常处理程序,它的任务是将程序从错误状态中恢复,以使程序能要么换一种方式运行,要么继续运行下去。
异常参数
与Java其他对象一样,我们总用new在堆上创建异常对象,这也伴随存储空间的分配和构造器的调用。构造器有两种:一个是默认构造器,另一个是接受字符串作为参数,以便能把相关信息放入异常对象的构造器:
throw new NullpointerException("t = null");
将new的对象交给throw后,将以抛出异常的方式从当前作用域退出,这与return有些相似,但相识之处也仅此而已。
捕获异常
try块
如果在方法块中抛出了异常,这个方法将在抛出异常的过程中结束,有时候我们希望后续的代码继续运行,我们可以将在方法内设置一个特殊的块来捕获异常:
try{
//Code that might generate exception||可能出现异常的代码
}
异常处理程序
抛出的异常必须在某处得到处理。这个地点就是异常处理程序,而且针对每个要捕获的异常,得准备相应的处理程序。异常处理程序紧跟在try块后,以关键字catch表示:
try{
//Code that might generate exception||可能出现异常的代码
}catch(Type1 id1){
//Handle exceptions of Type1||Type1的异常处理
}catch(Type2 id2){
//Handle exceptions of Type2||Type2的异常处理
}catch(Type3 id3){
//Handle exceptions of Type3||Type3的异常处理
}
//etc...
- 每个catch接收一个且仅接收一个特殊类型参数的方法,这与方法参数使用很相似。有时候可能用不到标识符,因为异常的类型已经给你足够的信息让你处理异常,但标识符并不能省略。
- 当异常被抛出的时候,异常处理机制将负责搜寻参数与异常类型相匹配的第一个处理程序,然后进入catch子句执行。
- 注意:只有匹配的catch才能执行,不匹配的catch将不执行
创建自定义异常
自己定义异常类,必须从已有的异常类继承,最好选择意思相近的异常类继承(不过这样的异常不好找)。新建的异常类型最简单的方法就是让编译器为你产生默认的构造器,这基本不用多少代码
Class MyException extends Exception{
}
然后你就可以使用throw方法抛出你新建的异常类了。
捕获所有异常
通过捕获异常类型的基类Exception可以做到捕获所有异常(事实上还有其他基类,但Exception是同编程活动相关的基类)
catch(Exception e){
System.out.println("Caught an exception");
}
最好将它放在处理程序表的末尾,防止它抢在其他处理程序之前先把异常捕获了。
Exception因为是基类,所以没有太多具体信息,不过可以调用它从其基类Throwable继承的方法:
- String getMessage()获取详细信息
- String getLocalizedMessage()获取本地语言表示的详细信息
- String toString()返回对Throwable的简单描述,要是有详细信息,也会将它包含在内
- void printStackTrace()打印Throwable和Throwable的调用栈轨迹,调用栈显示了“把你带到异常抛出地点”的方法调用序列,输出到标准错误,可以添加参数选择要输出的流(PrintStream|java.io.PrintWriter)
- Throwable fillInStackTrace()用于在Throwable对象的内部记录栈帧的当前状态,这在程序重新抛出错误或异常时很有用
栈轨迹:
printStackTrace()方法所提供的信息可以通过getStackTrace()方法来直接访问,这个方法将返回一个由栈轨迹中的元素所构成的数组,其中每一个元素都表示栈中的一帧。
重新抛出异常:
我们得到异常的引用后可以把它重新抛出
catch(Exception e){
System.out.println("");
thow e;
}
- 重抛异常会把异常抛给上一级环境中的异常处理程序,同一个try块的后续catch将被忽略。此外异常对象的信息都得以保持,高一级环境中捕获此异常可以得到这个异常的所有信息。
- 重抛异常后,printStackTrace()方法显示的僵尸原来异常抛出点的调用栈信息,而非重新抛出点的信息。可以用fillInStackTrace()方法,调用该方法的那一行就编程异常的新发生地了。
Java标准异常
Throwable这个Java类被用来表示任何可以作为异常被抛出的了。该类的对象可以分为两种类型(指从Throwable继承得到的类型):
- Error一般用来表示编译时和系统错误
- Exception是可以被抛出的基本类型,在Java类库、用户方法以及运行时故障中都可能抛出Exception型异常。我们关注的基本类型通常这是这一类
- 注意:只能在代码中忽略RuntimeException(及其子类)类型的异常,其他类型的异常处理都是由编译器强制实施的。
- 不应把Java的异常处理机制当成单一用的工具。它被设计用来处理一些烦人的运行时错误,这些错误往往是由代码控制能力之外的因素导致的,可以用于发现某些编译器无法检测到的编程错误。
使用finally进行清理
对于一些代码,可能会希望无论try块中的异常是否抛出,它们都能执行。比如:你打开了水龙头,无论它是否有出水,你在最后都应该把他关闭。这时候就可以使用finally:
try{
//Code that might generate exception||可能出现异常的代码
}catch(Type1 id1){
//Handle exceptions of Type1||Type1的异常处理
}catch(Type2 id2){
//Handle exceptions of Type2||Type2的异常处理
}catch(Type3 id3){
//Handle exceptions of Type3||Type3的异常处理
}finally{ //最后的处理语句}
//etc...
- 如果你在try或catch中使用了return、break、continue,finally子句也总会执行
构造器
在程序中,有些对象的创建成功与否关系到了代码的设计。拿FileReader类来讲,如果该类创建成功了,在使用完毕后我们需要将它释放掉(它占用着系统资源)。但如果它在创建的时候发生了错误,那我们就无法将它关闭(因为对象本身就不存在)。在谈怎么设计代码前我们先讲下构造器。构造器分可以失败的构造器与不能失败的构造器:可以失败的构造器即在构造方法后跟throws Exception的构造器,例:public A() throws MyException{} ;不能失败的构造器即不带异常抛出的构造器。
- 在遵循try-finally的原则下,对于不能失败的构造器就不需要任何catch,而对于可以失败的构造器,为了确保每个对象都能被清理,每一个对象构造必须将其包含在一个try-finally语句块中以确保清理。
public static void main(String[] arg){
try{
A a1 = new A();
try{
A a2 = new A();
try{
//....
}finally{
a2.disposed();
}
}catch(MyEception e){
System.out.println(e);
}finally{
a1.disposed();
}
}catch(MyEception e){
System.out.println(e);
}
}
- A类的构造器为可以失败的构造器
- 第一个try-finally块中如果a1创建失败将被catch,不会执行下面的清理代码,如果创建成功将在第二个try-finally的finally子句中被清理
- a2在第二个try-finally块中创建,如果创建失败将被第二个try-finally块catch,不会执行清理代码,如果正常运行将在第三个try-finally块被清理
这样将确保每个应该被清理的对象都被清理,没创建成功的对象不会执行清理代码而引发错误。
异常匹配
抛出异常时,异常处理系统会按照代码的书写顺序找出“最近”的处理程序。找到处理程序后将不继续查找(它认为异常被处理了)。简单点说就是catch语句的匹配,每个异常在try-finally块中被抛出时会匹配(符合参数类型)最近的catch子句。并且如果你的catch块的参数类型是抛出异常的基类,那么该异常也会被捕获。如果把捕获基类的catch子句放在最前面,会把派生类的异常全给“屏蔽”掉,但这样派生类的catch子句将不会得到执行,因此它将会报错。
异常使用指南
应该在下列情况下使用异常:
- 在恰当的级别处理问题。(在知道该如何处理的情况下才捕获异常)
- 解决问题并且重新调用产生异常的方法
- 进行少许修补,然后绕过异常发生的地方继续执行
- 用别的数据进行计算,以代替方法预计会返回的值
- 把当前运行环境下能做的事情尽量做完,然后把相同的异常重新抛到更高层
- 把当前运行环境下能做的事情尽量做完,然后把不同的异常重新抛到更高层
- 终止程序
- 进行简化(如果你的异常模式使得问题太复杂,那异常机制用起来会非常痛苦、烦人)
- 让库类和程序更安全。(这既是在为调试做短期投资,也是在为程序的健壮性做长期投资)
总结
异常是Java程序不可分割的一部分,如果不了解如何使用它们,那你只能完成很有限的工作。许多类库(例如I/O库),如果不处理异常,你就无法使用它们。
异常处理的优点之一就是它使得你可以在某处集中精力处理你要解决的问题,而在另一处处理你编写这段代码中的错误。Java将所有的错误都以异常形式报告,这正是它远超诸如C++这类语音的长处之一(C++这类语言需要以大量不同的方式来报告错误,或者根本没有提供错误报告功能)。
以上总结自《Java编程思想(Thinking in Java)》——YayayaHong