当程序由于出现错误而使得某些操作没有完成,应该返回 到一种安全状态,并能够让用户执行一些其他的命令。
允许用户保存所有的操作的结果,并以适当的方式终于程序。
对于方法中出现 的错误,传统的处理方式是返回一个特定的错误编码,调用这个方法的方法对其进行分析。例如,对于一个从文件中读取信息的方法来说,如果返回值不是标准字符,则是一个-1,则表示文件结束。还有一种表示错误状况的常用返回值是null引用。这种处理对于很于很多异常状况都是可行的。
但是并不是所有情况都能够返回一个错误的编码。有可能无法明确将有效数据与无效数据加以区分。一个返回整形的方法就不能简单地通过返回-1表示错误,因为-1很可能是一个完成合法的结果。
在Java中,如果某个方法不能采用正常的途径完成它的任务,就可以通过另外一个路径退出方法。在这种情况下,方法并不返回任务值,而抛出一个封装了错误信息的对象。
注意:这个方法立该退出,并不返回任何值。此外,调用这个方法的代码也将无法继续执行,而是,异常处理机制开始搜索能够处理这种异常状况的异常处理器。
public static void main(String[] args) {
System.out.println("调用前");
throwException();
System.out.println("调用后");
}
public static void throwException(){
try{
System.out.println("出错前");
double a = 1/0;
System.out.println("出错后");
}
catch(ArithmeticException e){
e.printStackTrace();
}
}
如上,main方法调用throwException方法,throwException方法出现异常并自行处理,则mian可以继续执行。结果如下:
public static void main(String[] args) {
System.out.println("调用前");
try{
double result = throwException();
System.out.println(result);
System.out.println("调用后");
}
catch(ArithmeticException e){
e.printStackTrace();
}
}
public static double throwException() throws ArithmeticException{
System.out.println("出错前");
double a = 1/0;
System.out.println("出错后");
return a;
}
如上所示,throwException方法声明了可能抛出一个异常,但没有自己处理,则main方法在throwException方法抛出方法后就无法执行了。如果如下:
异常分类
所有的异常都是由Throwable继承而来,但在下一层立即分为:Error和Exception。
Error类层次结构描述了Java运行时系统的内部错误和资源耗尽错误。应用程序不应该抛出这种类型的对象。如果出现了这样的内部错误,除了通告用户,并尽力使程序安全终止之外,再也无能为力了。这种情况很少出现。
注意:Exception又分解为两个分支。一个分支派生于RuntimeException,另一个分支句含其他异常。划分两个分支的规则是:由程序错误导致的异常属于RuntimeException,而程序本身没有问题,但由于像I/O错误这类问题导致的异常属于其他异常。
派生于RuntimeException的异常有:
错误的类型转换
数组访问越界
访问空指针
不是派生于RuntimeException的异常包括
试图在文件尾部后面读取数据
试图打开一个错误格式的URL
试图根据给定的字符串查找Class对象,而这个字符串表示的类并不存在。
“如果出现RuntimeException异常,那么就一定是你的问题”是 一条相当有道理的规则。应该通过检测数组下标是否越界来避免ArrayIndexOutOfBoundsException异常,应该通过在使用变量之前检测是否为空来杜绝NullPointerException异常的发生。
Java语言规范将派生于Error类或RuntimeException类的所有异常称为未检查异常,所有其他的异常称为已检查异常。
声明已检查异常
以下情况就用thorws子名声明抛出异常:
1.调用 一个抛出已检查异常的方法,如FileInputStream构造器
2.程序运行过程发现错误,并且利用throw语句抛出一个已检查异常
3.程序出现错误,如a[-1] = 0 会抛出一个ArrayIndexOutOfBoundsException这样的未检查异常。
4.Java虚拟机和运行时库出现的内部异常。
如果出现前两中情况之一,则必须告诉调用这个方法的程序员有可能抛出异常。因为任何一个抛出异常的方法都有可能是一个死亡陷阱。如果没有处理器捕获这个异常,当前执行的线程就会结束。
注意:不需要声明Java的内部错误,即从Error继承的错误。任何程序代码都具有抛出那些异常的潜能,而我们对其没有任何控制能力。
同样,也不应该声明从RuntimeException继承的那些未检查异常。如
class MyAnimaction{
...
void drawImage(int i) throws ArrayIndexOutOfBoundsException //bad style{
...
}
}
这些运行时错误完全在我们的控制之下。如果特别关注数组下标引发的错误,就应该将更多的时间花费在修正程序的错误中,而不是说明这些错误发生的可能性上。
总之,一个方法必须声明所有可能抛出的已检查异常,而未检查异常要么不可控制,要么就应该避免发生。如果方法没有声明所有可能发生的已检查异常,编译器就会给出一个错误消息。
注意:如果在子类中覆盖了超类的一个方法,子类方法中声明的已检查异常不能超过超类方法中声明的异常范围(也就是说,子类方法中抛出的异常范围更加小,或者根本不抛出任何异常)。特别需要说明 的是,如果超类方法没有抛出任何已检查异常,则子类也不能抛出任何已检查异常。
如下:定义了一个只有一个read方法的File类
public class File {
public void read(){
...
}
}
而它的子类重写read方法,并声明抛出异常FileNotFoundException,则它无法通过编译。
public class InputStringFile extends File{
@Override
public void read() throws FileNotFoundException{ //Exception FileNotFoundException is not compatible with throws clause in File.read()
super.read();
}
}
对于一个已经存在的异常类,将其抛出非常容易。在这种情况下:
1.找到一个合适的异常类。
2.创建这个类的一个对象。
3.将对象抛出。
一旦方法抛出了异常,这个方法就不可能返回调用者,也就是说,不必为返回值的默认值或错误代码担忧。
创建异常类
定义一个派生于IOException的类。习惯上,定义 的类应该包含两个构造器,一个是默认的构造器;另一个是带有详细描述信息的构造器(超类Throwable的toString方法将会打印出这些详细信息,这在调度中非常有用。)
class FileFormatException extends IOException{
public FileFormatException (){};
public FileFormatException (String gripe){
super(gripe);
}
}
现在,就可以抛出自己定义 的异常类型了。
String readData(BufferedReader in) throws FileFormatException {
...
while(...){
if(ch == -1){//EOF encountered
if(n < len) throw new FileFormatException ();
}
}
}
捕获异常
捕获异常需要进行周密的计划。
如果某个异常发生的时候没有在任何地方捕获,那程序就会终止执行,并在控制台上打印异常信息,其中包括异常的类型和堆栈信息。
如果在try语句中的任何代码抛出了一个在catch子句中说明的异常类,那么
1.程序将跳过try语句块的其余代码 。
2.程序将执行catch子句中的处理器代码。
如果在try语句块中的代码没有抛出任何异常,那么程序将跳过catch子句。
如果方法中的任何代码抛出了一个在catch子句中没有声明的异常类型,那个这个方法就会立刻退出(期待调用者为这种类型的异常设计 了catch子句)
通常,应该捕获那些知道如何处理的异常,而将那些不知道怎样处理的异常传递出去。
仔细阅读一下Java API文档,以便知道每个方法可能会抛出哪种异常,然后再决定是自己处理还是添加到throws列表中,对于后一种情况,也不必犹豫。将异常直接交给能够胜任的处理器进行处理要比压制对它的处理更好。
同时也记住,如果编写一个覆盖超类的方法,而超类这个方法又没有抛出异常,那个子类这个方法就必须捕获方法代码中出现的每一个已检查异常。不允许在子类的throws说明符中出现超过超类方法所列出的异常类范围。
finally
强烈建议独立使用try/catch和try/finally语句块。这样可以提高代码的清晰度
InputString in = ...;
try{
try{
}
finally{
in.close();
}
}
catch(IOException e){
show error dialog
}
内层的try语句块只有一个职责,就是确保关闭输入流。外层的try语句块也只有一个职责,就是确保报告中出现的错误。这种设计不仅清楚,而且还具有一个功能,就是将会报告finally子句中出现的错误。
当finally子句包含return 语句时,将会出现一种意想不到的结果。假设利用return语句从try语句块中退出。在方法返回前,finally子句的内容将被执行。如果finally子句也有一个return语句,这个返回值将会覆盖原始值。如:
public static int f(int n){
try{
int r = n * n ;
return r;
}
finally{
if(n == 2)
return 0;
}
}
如果调用f(2),那try语句块的计算结果为r=4,并执行return 语句。然而,在方法真正返回前,还要执行finally子句。finally子句将使方法返回0,这个返回值覆盖了原始的返回值4.
使用异常机制的建议
1.异常处理不能代替简单的测试。
捕获异常花费时间比较多,因此使用异常的基本规则是:只在异常情况下使用异常机制。
2.不要过分地细分异常
OutputStream out;
for(int i = 0; i < 100; i++){
try{
n = s.pop();
}
catch(EmptyStackException s){
//statck war empty
}
try{
out.writeInt(n);
}
catch(IOException e){
//problem writing to file
}
}
这种编程方式将导致代码量急剧膨胀。而当s为空时,捕获异常后也没有处理异常。应该将整个任务包装在一个try语句块,这样,当任何一个操作出现问题时,整个任务都可以取消。
OutputStream out;
for(int i = 0; i < 100; i++){
try{
n = s.pop();
out.writeInt(n);
}
catch(IOException e){
//problem writing to file
}
catch(EmptyStackException s){
//statck war empty
}
}
3.利用异常层次结构
不要只抛出RuntimeException异常。应该寻找更加适当的子类或创建自己的异常类。
不要只捕获Throwable异常,否则,会使程序代码更难读,更难维护。
将一种异常转换成另一种更加适合的异常时不要犹豫。如,在解析某个文件中的一个整数时,捕获NumberFormatException异常,然后将它转换成IOException或MySubsystemException的子类 。
4.不要压制异常
如果一个访求可能100年才抛出一个异常,可以将这个异常关闭,而不必要求所有调用这个方法的方法都进行异常处理。
public Image loadImage(String s){
try{
code that threadtens to throw checked exception
}
catch(Exception e){
}
}
这样,这段代码可以通过编译了。除非发生异常,否则它都可以正常运行。即使发生异常也会忽略。但如是异常非常重要,就应该对它们进行处理。
5.在检测错误时,严格比宽松更好。
在用无效参数调用 一个方法时,返回一个虚拟的数值还是抛出一个异常,哪个处理方式更好。如,当栈空是地,Stack.pop是返回一个null,还是抛出一个异常?
在出错的地方抛出一个EmptyStackException异常比在后面抛出一个NullPointerException异常更好 。
6.不要羞于传递异常
当不知道怎样处理一个异常时,传递异常比捕获异常更好。
让高层次的方法通告用户发生了错误,或者放弃不成功的操作更加合适。
早抛出,晚捕获