Java编程思想 第十二章:通过异常处理错误
发现错误的理想时机是在编译阶段,也就是程序在编码过程中发现错误,然而一些业务逻辑错误,编译器并不能一定会找到错误,余下的问题需要在程序运行期间解决,这就需要发生错误的地方能够准确的将错误信息传递给某个接收者,以便接收者知道如何正确的处理这个错误信息。
改进错误的机制在Java中尤为重要,Java使用异常来提供一致性的错误报告,使得程序构件可以与客户端代码可靠地沟通问题所在。本文学习如何编写正确的异常处理程序,并将展示方法出现问题时,如何自定义异常。
1.概念
C语言以及其它早期语言的错误处理方式通常是一些约定俗成的模式,比如返回某个错误标记或者设置某个特殊的值通过判断值来确认是否发生了错误。然而长期来看这种错误处理方式由于需要大量的判断以及细致的错误检查而使代码逻辑较为复杂,因此不利于构建大型健壮性的系统。
解决的方法是,用强制规定的形式来消除错误处理过程中随心所欲的因素。“异常”这个词有“我对此感到意外”的意思,当错误问题出现了,你可能不知道出现在哪里,或者出现了什么错误,也不知道怎么解决。那么就停下来,将这个问题提供给更高的环境,看看有没有正确的解决方案。使用异常的另一个好处是,它能够明显的降低代码的复杂程度,避免了大量的错误检查,只需要在一个特定的地方进行异常捕获,并且不需要做任何判断,异常捕获区能够捕获所有发生的错误。这种异常的处理方式与之前的错误处理方式相比,完全的将“正常做的事儿”与“出现问题怎么办”隔离开来。是代码的读写变得更加井井有条
2. 基本异常
“异常情形”是指阻止当前方法或者作用域继续执行的问题,要把异常情形与普通问题区分开来,普通问题是指在当前的错误收集情况下,能够解决这个问题并继续执行正常的程序。而对于异常情形,这个程序就不能够正常的执行下去了。
除法是一个简单的例子,除数有可能为0,所以先进行检查很有必要。但除数为0如果是一个意外的值,你也不清楚该如何处理,那么抛出异常就显得尤为重要了,而不是顺着原来的路走下去。
抛出异常的内核本质:
- 同 Java中其它对象的创建相同,将使用new在堆上创建对象。
- 当前程序的执行路径被终止了,因为发生了异常,不能继续执行下去,并且从当前执行的环境中弹出异常对象的引用。
- 此时,异常处理机制接管程序,并开始寻找一个恰当的地方继续执行程序,这个恰当的地方就是异常处理程序。它的任务是使程序从错误的状态中恢复,要么换一种方式运行,要么继续运行下去。
举一个抛出异常的简单例子,对于一个对象引用t,传递给你的时候可能没有被初始化,所以在使用这个对象引用调用执行方法之前,进行合理的判断是非常有必要的。可以创建一个代表错误信息的对象,并且将它从当前环境中抛出,这样就把错误异常抛到更大的环境中去了。所以一个异常,看起来是这样的:
if(t==null){
throw new NullPointerException();
}
异常情形是指阻止当前方法或作用域继续执行的问题。
异常处理程序将程序从错误状态中恢复,以使程序要么换一种方式运行,要么继续运行下去。
在没有其它办法的情况下,异常允许我们强制程序停止运行,并告诉我们出现了什么问题。理想状态下,还可以强制程序处理问题,并返回到稳定状态的。
3.捕获异常
监控区域是一个可能产生异常的代码,并且后面跟着处理这些异常的代码。
如果在方法内部抛出了异常,那么这个方法就此结束。如果不希望这个方法结束,那么可以在方法内设置一个特殊的块来捕获异常,即try块。为什么叫try呢,因为在这个块里“尝试”各种可能产生异常的方法进行调用,所以是try。
try {
// Code that might generate exceptions
} catch(Type1 id1)|{
// Handle exceptions of Type1
} catch(Type2 id2) {
// Handle exceptions of Type2
} catch(Type3 id3) {
// Handle exceptions of Type3
}
异常抛出后,异常处理机制将搜索参数与异常类型相匹配的第一个处理程序,进入catch语句处理,此时认为异常的到了处理。catch子句结束,则处理程序不再往下找匹配了。
异常处理理论上有两种基本模型:
- java支持终止模型。该模型假设错误非常关键,一旦异常被抛出,那么错误已经无可挽回,程序不能继续执行。
- 恢复模型,就是先修正错误,然后重新进入该方法。这个模型假定了修正完之后再进入执行一定会成功。
4.创建自定义异常
可以异常类不写构造函数,使用默认无参构造函数,也可以写构造函数。酱紫可以实现在抛出的异常后面打印出异常所在函数等功能。比如:
class MyException extends Exception {
public MyException() {}
public MyException(String msg) { super(msg); }
}
在抛出异常时
public static void g() throws MyException {
System.out.println("Throwing MyException from g()");
throw new MyException("Originated in g()");
}
那么,在打印的时候,就可以打印出
MyException: Originated in g()
5.异常说明
Java鼓励人们把方法可能会抛出的异常告知使用此方法的客户端程序员。异常说明使用了附加的关键字throws,后面接一个所有异常在异常类型的列表,所以方法定义如下:
void f() throws TooBig, TooSmall, DivZero { //...
这种在编译时被强制检查的异常称为被检查的异常。
也可以声明方法将抛出异常,但是实际上却不抛出。这样做可以先为异常占个位置,以后可以抛出这类异常而不用修改已有方法,这种“作弊”方法通常用在你定义抽象基类和接口时,这样派生类或者接口实现就能抛出这些预先声明的异常。
6.捕获所有异常
捕获异常类型的基类Exception(还有其它基类),这可以保证异常一定会被捕获,最好把它放到异常处理程序列表的末尾。
catch(Exception e) {
System.out.println("Caught an exception");
}
Exception可以调用其从基类继承的方法:
- String getMessage( )
- String getLocalizedMessage( )
获取详细信息(抛出异常对象所带的参数),或者用本地语言表示的详细信息。
打印Throwable和Throwable的调用栈轨迹:
- void printStackTrace( )
- void printStackTrace(PrintStream)
- void printStackTrace(java.io.PrintWriter)
6.1 栈轨迹
printStackTrace()方法所提供的信息可以通过getStackTrace()方法来直接访问,该方法返回一个由栈轨迹元素所构成的数组,每个元素表示栈中的一帧,元素0也是栈顶元素,是最后调用的方法(Throwable被创建和抛出之处),最后一个元素是栈底,是调用序列的第一个方法调用。
6.2 重新抛出异常
挡在异常处理模块里继续抛出异常,那么printStackTrace()方法显示的将是原来异常抛出点的调用栈信息,而非重新抛出点的的信息。
catch(Exception e) {
System.out.println("An exception was thrown");
throw e;
}
此时可以使用fillinStackTrace()方法
catch(Exception e) {
System.out.println("An exception was thrown");
throw (Exception)e.fillInStackTrace();
}
调用fillInStackTrace()的这一行就成为异常的新发生地了。在异常捕获之后抛出另一种异常,其效果类似于fillInStackTrace()。
7.Java标准异常
hTrowable对象可分为两种类型(指从Throwable继承而得到的类型):Error用来表示编译时和系统错误,Exception是可以被抛出的基本类型,包括Java类库,用户方法以及运行时故障都可以抛出此异常。
Error一般不用自己关心,现在来讲Exception:
7.1 特例RuntimeException
比如nullPointerException,空指针异常。运行时产生的异常,不需要在异常说明中声明方法将抛出RuntimeException类型的异常。它们被称为“不受检查的异常”。这种异常属于错误,会被自动捕获,而不用程序员自己写代码捕获。
如果RuntimeException没有被捕获而直达main(),那么在程序退出前将调用异常的printStackTrace()方法。
8.使用finally进行清理
在异常处理程序后面加上finamlly子句,可保证无论try块里的异常是否抛出,都能执行。(通常适用于内存回收之外的情况)
finally执行未必要放在最后,正常的顺序执行到它就是它了。
try {
// The guarded region: Dangerous activities
// that might throw A, B, or C
} catch(A a1) {
// Handler for situation A
} catch(B b1) {
// Handler for situation B
} catch(C c1) {
// Handler for situation C
} finally {
// Activities that happen every time
}
9. 异常的限制
当覆盖 方法时,只能抛出在基类方法的异常说明里列出的那些异常。这个限制意味着,当基类代码运用到派生类时,依旧有用。
当处理派生类对象时,编译器只会强制要求捕获派生类该方法产生的异常。如果向上转型为基类,编译器会要求捕获基类方法产生的异常。很智能的。
异常说明本身并不属于方法类型的范畴中,因此不参与重载的判断。
基于特定方法的“异常说明的接口”不是变大了而是变小了,小于等于基类异常说明表——这恰好和类接口在继承时的情形相反。
10.构造器
如果在构造器中抛出了异常,这些清理行为也就不会正常工作了。这就意味着在编写构造器时要格外小心。有人可能认为使用finally就可以解决问题了。但问题并不是那么简单,因为finally每次都会执行清理代码。如果构造器在其执行过程中半途而废,也许该对象的某些部分还没有成功创建,而这些部分在finally子句中却是要别清理的。
11. 异常的匹配
上一篇: hua_性能知识点