详解 异常(exception) 二

异常大的分类分为检测性异常(checked exception)和非检测性异常(unchecked exception)。其中检测性异常是指在编译器就能够发现的异常。

非检测性异常又包括Error 和 Exception  其中Error代表了非程序内部的异常,即来自于外部的异常:比如: 硬盘破坏、系统故障等。 Exception是指程序内部的异常:一般是指程序是逻辑出了问题,或者 不正确的使用了API造成的异常。

不管是检测性异常还是非检测性异常 都是继承自 Throwable类。

一:方法抛出异常

      上一节中学习了在ListOfNumbers类中的writeList方法写异常处理程序。有时候在方法内部捕获是合适的。但是在其他有些情况下,它是更合理的让一个方法进一步的调用栈去处理这个异常(it's better to let a method further up the call stack handle the exception),即不在该方法中处理异常,而是抛给调用这个方法的方法去处理。例如,如果你提供ListOfNumbers类作为包类的一部分,你有可能不预先得知所有使用该包的用户,即你不知道哪些其他的用户会使用这个类,有可能其他包下的类会使用这个类,即使用这个类的情况不确定,在这种情况下,不捕获异常也许是一个更好的方式,而是让进一步调用这个方法的调用栈去处理它。
   如果writeList()方法不捕获出现在其内部的检测性异常,writeList方法必须指定它能够抛出这些异常,下面修改writeList方法抛出一个异常代替捕获异常,以下是writeList方法不能通过编译的原始版本。
public void writeList() {
	    PrintWriter out = new PrintWriter(new FileWriter("OutFile.txt"));
	    for (int i = 0; i < SIZE; i++) {
	        out.println("Value at: " + i + " = " + list.get(i));
	    }
	    out.close();
	}

为了定义writeList方法抛出两个异常,在writeList的方法声明中增加throws语句,如下所示:
	
	public void writeList() throws IOException, ArrayIndexOutOfBoundsException {
	    PrintWriter out = new PrintWriter(new FileWriter("OutFile.txt"));
	    for (int i = 0; i < SIZE; i++) {
	        out.println("Value at: " + i + " = " + list.get(i));
	    }
	    out.close();
	}

注意:ArrayIndexOutOfBoundsException是一个非检测性异常,不强制抛出这个异常,像下面这样写也是可以的。
public void writeList() throws IOException {
	    PrintWriter out = new PrintWriter(new FileWriter("OutFile.txt"));
	    for (int i = 0; i < SIZE; i++) {
	        out.println("Value at: " + i + " = " + list.get(i));
	    }
	    out.close();
	}

二:如何抛出异常

在你捕获异常之前,一定有一些代码抛出了一些异常,抛出异常的代码包括:你自己的代码,其他人写在java平台的包中的代码,或者java 运行时环境,不管谁抛出异常,总是会有throw语句进行修饰。

你也许已经注意到,java 平台提供各种各样的异常类,所有的异常类都是Throwable类的子类,在程序执行期间,异常类出现在哪里是区分各种异常类的一个方法。

你能够创建自己的异常类声明出现在你写的类中的问题,如果你开发了一个包,你也许不得不创建自己的异常类允许用户区分来自你包你的错误和java平台下别的包的错误,你也能创建异常链。

1:抛出语句

所有的方法使用throw语句抛出一个异常,throw语句需要一个单独的参数,一个throwable对象,Throwable对象是Throwable类的子类,下面是一个throw语句的例子。
throw someThrowableObject;
来看一下throw语句的用法,pop方法是一个实现了常用的stack对象的类中的方法,这个方法从栈的顶端移除一个对象,并且把这个对象返回,代码如下:
public Object pop() {
	    Object obj;

	    if (size == 0) {
	        throw new EmptyStackException();
	    }

	    obj = objectAt(size - 1);
	    setObjectAt(size - 1, null);
	    size--;
	    return obj;
	}
pop方法检查在栈中是否还有一些元素,如果栈是空的,pop方法实例化了一个new EmptyStackException对象(java.util下的成员类),和抛出它,需要注意的是你仅仅能够抛出继承自Throwable类的对象。

注意:方法的声明中不包含一个throws语句,EmptyStackException 异常不是一个检测性异常,因此pop方法没有必要声明它的出现。

2:Throwable类和它的子类

从Throwable类继承的类包括直接从Throwable类继承和间接从Throwable类继承,如下图所示:Throwable类有两个直接子类:Error 和  Exception

                  Throwable类及其子类结构图

3:Error类

当JVM出现动态链接失败或者其他的难处理的失败时,JVM会抛出一个Error,简单的程序通常不能够捕获或者throw Errors。

4:Exception 类

大部分程序抛出或者捕获的对象源自于Exception类,一个Exception类说明了一个问题的出现,但是它不是一个严重的系统级问题,大部分写程序的时候和Errors的处理方式相反,是抛出或者捕获这类异常。

java平台定义了很多Exception的子类,这些子类显示了可能出现的各种各样的异常,例如IllegalAccessException说明没有找到一个特定的方法。NegativeArraySizeException说明了一个程序视图创建一个负数大小的数组。

Exception的一个子类RuntimeException是说明了不正确的使用API,通常是使用过程中逻辑出现了问题,例如NullPointerException是一个运行时异常,当一个方法试着访问一个null引用的时候会出现这种异常。

二:异常链(Chained Exceptions

程序中经常通过抛出一个异常的方法来回应一个异常。事实上是第一个异常引起了第二个异常,知道当一个异常引起了另外一个异常这个信息非常重要。异常链(Chained Exceptions)会帮助我们做这件事情。

下面是一些在Throwable类中支持链式异常的方法和构造方法
Throwable getCause()
Throwable initCause(Throwable)
Throwable(String, Throwable)
Throwable(Throwable)

在 initCause方法和Throwable构造方法中的Throwable类型的参数代表了引起当前的当前异常。
getCause方法返回引起的当前异常。
下列代码显示了怎么使用异常链:
try {

} catch (IOException e) {
    throw new SampleException("Other IOException", e);
}

在这个例子中IOException异常被引起时,带有原始动因的 new  SampleException  异常对象被创建,添加到异常链的后面,抛出到更高级的异常去处理。

1:访问栈追踪信息(Stack Trace Information

现在假设更高级的异常处想以自己的格式打印栈跟踪信息。
定义(Definition):栈跟踪(stack trace)提供当前线程执行历史的信息,列出在出现异常时所调用的类名和方法名,栈跟踪是一种有用的调试工具,当一个异常抛出的时候,你将看到它的好处。

下列代码演示了怎样调用异常对象的getStackTrace方法:
package test.exception;

public class DivisionZero {
	public static void main(String[] args) {
		try{
			System.out.println(5/0);
		}catch (Exception cause) {
		    StackTraceElement elements[] = cause.getStackTrace();
		    for (int i = 0, n = elements.length; i < n; i++) {       
		        System.err.println(elements[i].getFileName()
		            + ":" + elements[i].getLineNumber() 
		            + ">> "
		            + elements[i].getMethodName() + "()");
		    }
		}
	}
}

以上程序能够打印出相应的信息。

2:Logging API

下列例子是把异常信息输出到OutFile.log文件中:
package test.exception;

import java.io.IOException;
import java.util.logging.FileHandler;
import java.util.logging.Handler;
import java.util.logging.Level;
import java.util.logging.Logger;

public class DivisionZero {
	public static void main(String[] args) {
		
		try {
			System.out.println(5/0);
		    Handler handler = new FileHandler("src/resource/OutFile.log");
		    Logger.getLogger("").addHandler(handler);
		    
		} catch (IOException e) {
		    Logger logger = Logger.getLogger("package.name"); 
		    StackTraceElement elements[] = e.getStackTrace();
		    for (int i = 0, n = elements.length; i < n; i++) {
		        logger.log(Level.WARNING, elements[i].getMethodName());
		    }
		}
	}
}

三:创建一个异常类

当面对选择抛出的异常类的时候,你能使用一些别人写好的类---java平台提供了很多你能使用的异常类,或者你可以写一个你自己的异常类,如果下列问题的回答都是 ‘是’你能创建自己的异常类。否则,你能使用别人的异常类。
1:你需要的异常类型在java平台中没有相关的描述:?
2:别的供应商或者调用者能够从被抛出的异常中区分出你的异常?
3:代码中抛出超过一个相关的异常?
4:如果你使用一些别的异常,用户将访问这些异常吗?一个相似的问题,你的包是独立的吗?


1:一个例子

假设你写了一个linked list 的类,这个类支持下列方法:
  • objectAt(int n) —返回在第n个位置的对象,如果这个参数小于0或者大于list中对象的总数抛出一个异常.
  • firstObject() — 返回第一个对象,当list容器中没有对象的时候,抛出一个异常,
  • indexOf(Object o) —查找特定对象的索引,返回该对象在list中的位置,如果list中不存在这个对象,抛出一个异常。
lined list类能够抛出很多异常,通过linked list的一个异常处理程序捕获所有抛出的异常是一件很容易的事情。
如果计划在一个包内描述linked list,所有相关的代码都应该在包内,因此linked list应该提供它的一系列的异常类。
下图是对 linked list 异常类及其子类的结构描述。

异常类继承的示例

2:选择一个子类

能够使用LinkedListException的地方也能够使用一些其他的子类,当熟悉这些子类以后,很多情况下使用使用的异常和LinkedListException父类完全不相关,因此LinkedListException父类是一个例外,一般不用。
大部分写的程序中抛出异常对象的类型是Exceptions,Errors通常是一种较严重的情况,如果系统硬件破坏,系统故障等,这些都将阻止JVM的运行。

注意:为了代码的可读性,在所有间接或者直接继承自Exception类的子类中追加异常字符串名字是一个很好的编程习惯。

四:关于非检测性异常的争论(Unchecked Exceptions — The Controversy


由于java语言不需要方法捕获或者指定特定的非检测性异常(RuntimeException,Error,和它们的子类),程序员也许有兴趣在代码中仅仅抛出非检测性异常,或者让它们所有的异常子类继承自RuntimeException,这两种方式都让程序员没有编译错误和没有捕获指定异常的干扰,虽然这似乎对程序员来说很方便,但是当别人使用你写的类的时候也许会引起一些问题。因此不建议这样操作。

为什么设计者强制指定所有在一个方法内抛出的检测性异常呢?在一个方法上抛出异常作为该方法公共编程接口的一部分。方法的调用者必须知道方法抛出了什么异常,为了让调用者决定关于这个方法需要怎么做,在方法的编程接口中,异常是作为方法的参数和返回值的一部分。

下面一个问题是:如果一个方法API文档写的很完善,包括了它抛出的异常,为什么在这个方法中不指定运行时异常呢?运行时异常说明了程序运行时出现的问题,对于这些异常问题客户端代码不能合理的从这些异常问题中恢复,或者不能才起一些其他的处理方式处理这些异常问题。这些异常问题包括算术运算异常(例如除0);指向(引用)异常(例如试图访问一个指向空对象的引用);索引异常(例如试图访问一个太大或者太小的数组的索引)。

运行时异常能够出现在程序中的很多地方,换句话来说,就是有很多各式各样的运行时异常,在每一个方法声明中添加运行时异常将减少程序的透明性(使程序不是很清晰),为了使程序更加清晰、明了,因此编译器不是必须让程序员捕获或者指定特定的异常(即使你能够捕获或指定)。

常见的抛出运行时异常的例子是当用户错误的调用一个方法的时候,例如:如果一个参数是不正确的为null,方法能够检查到, 如果一个参数为null,方法也许会抛出一个NullPointerException,该异常是个非检测性异常(unchecked  exception)。


一般来说:不会抛出运行时异常或者简单的创建一个RuntimeException的子类,因为你不想在方法中指定需要抛出异常而感到烦恼。

下面是一条重要的规则:如果客户端代码能够合理的从异常中恢复,该异常就是一个检测性异常(checked exception),如果不能合理的从异常中恢复,该异常就是一个非检测性异常(unchecked exception)。

五:使用异常的优点

以上已经了解了异常,以及如何使用异常,下面来看一下在程序中使用异常的好处。

1:从业务规则代码中分离出错误处理代码

当在一个主要逻辑代码中出现非常规性事件,异常从代码中分离出来,提供有意义的信息。

在传统的编程中,错误检测、报告和处理经常让代码感觉到混乱,例如:下面是一个读文件到内存中的伪代码方法。

readFile {
    open the file;
    determine its size;
    allocate that much memory;
    read the file into memory;
    close the file;
}

初看之下:这个函数很简单,但是它忽略了所有的下列可能的错误:
  • 如果这个文件不能够打开将要发生什么?
  • 如果不能够决定文件的长度会发生什么?
  • 如果没有足够的内存去分配怎么办?
  • 如果读取这个文件失败了怎么办?
  • 如果这个文件不能够关闭怎么办?

为了处理以上问题,readFile函数必须有更多的代码去做错误检测、报告和处理错误,下面是一个有可能的函数的例子。
errorCodeType readFile {
    initialize errorCode = 0;
    
    open the file;
    if (theFileIsOpen) {
        determine the length of the file;
        if (gotTheFileLength) {
            allocate that much memory;
            if (gotEnoughMemory) {
                read the file into memory;
                if (readFailed) {
                    errorCode = -1;
                }
            } else {
                errorCode = -2;
            }
        } else {
            errorCode = -3;
        }
        close the file;
        if (theFileDidntClose && errorCode == 0) {
            errorCode = -4;
        } else {
            errorCode = errorCode and -4;
        }
    } else {
        errorCode = -5;
    }
    return errorCode;
}

有很多需要错误检测、报告和处理的地方,让代码看起来很凌乱,更糟的是,代码的逻辑也变得模糊了,因此不是很好理解以上代码在做什么?很多程序员选择忽略的方式解决以上的问题,但是很不可取。

异常能够让您把主要的流程写出来,在其他别的地方统一处理这些异常,如果以上例子采用异常的方式去处理,如下所示:
readFile {
    try {
        open the file;
        determine its size;
        allocate that much memory;
        read the file into memory;
        close the file;
    } catch (fileOpenFailed) {
       doSomething;
    } catch (sizeDeterminationFailed) {
        doSomething;
    } catch (memoryAllocationFailed) {
        doSomething;
    } catch (readFailed) {
        doSomething;
    } catch (fileCloseFailed) {
        doSomething;
    }
}

注意:异常不分散解决主要逻辑的精力,但是它们能够帮助你更有效的组织代码。

2:传播Errors(错误)到调用栈(the call stack)

能够传播错误直到方法的调用栈,假设readFile方法在一些列主程序的方法嵌套调用中是第四个方法,method1调用method2,method2调用method3,method3调用 readFile,如下所示:
method1 {
    call method2;
}

method2 {
    call method3;
}

method3 {
    call readFile;
}

假设只有method1在乎 readFile方法中出现的异常, 常见的错误通知技术强制method2和method3传播readFile方法返回的错误代码到调用栈的顶端直到最后到达method1,因为只有method1关心readFile方法的异常。
调用模式如下:
method1 {
    errorCodeType error;
    error = call method2;
    if (error)
        doErrorProcessing;
    else
        proceed;
}

errorCodeType method2 {
    errorCodeType error;
    error = call method3;
    if (error)
        return error;
    else
        proceed;
}

errorCodeType method3 {
    errorCodeType error;
    error = call readFile;
    if (error)
        return error;
    else
        proceed;
}

当调用的时候,JVM向后搜索调用栈找到一些关心处理特定异常的方法(method1),一些其他的方法能够继续压入异常在它的方法内部,因此允许方法进一步到调用栈中去捕获它,因此只有这个需要检测的方法关心这个调用。

method1 {
    try {
        call method2;
    } catch (exception e) {
        doErrorProcessing;
    }
}

method2 throws exception {
    call method3;
}

method3 throws exception {
    call readFile;
}

上面的方法显示:异常能够抛出通过在方法的声明中指定特定的 throws语句。

3:分类明确,区分Error 类型

因为所有在程序内部抛出的异常是对象,异常的分类自然是类继承的结果,在java平台中涉及到异常类分类的例子在
java.io包中,IOException和它的子类,当需要I/O的时候,IOException是一个常见的声明,它的子类代表了更多特定的错误,例如FileNotFoundException代表了在硬盘的特定路径位置下没有找到文件的错误。
catch (FileNotFoundException e) {
    ...
}

一些方法中能够捕获分类后的普通异常通过在catch语句中指定特定的异常的超类。
例如捕获所有的I/O异常,尽管有很多特定类型,但是异常处理程序只需要指定IOException就能够捕获所有的I/O异常。如下:
catch (IOException e) {
    ...
}
以上处理将能够捕获所有的I/O异常,包括FileNotFoundException,EOFException,等等,能够在catch语句块中查看更详细的信息通过以下方式:打印这个stack  trace(栈追踪)。
catch (IOException e) {
    // Output goes to System.err.
    e.printStackTrace();
    // Send trace to stdout.
    e.printStackTrace(System.out);
}

你甚至能够像以下这样处理异常:
// A (too) general exception handler
catch (Exception e) {
    ...
}
Exception是Throwable类的直接子类,因此异常程序将捕获所有的异常,只要是异常处理程序想捕获,如果你想让你的程序捕获所有的异常,你能够通过这种方式。然后打印出错误信息退出。

在大部分情况下,异常处理程序应该捕获特定的异常,最好的恢复策略是通过执行捕获特定的异常来实现的。事实上,不捕获特定的异常,异常处理程序必须适应一切可能的异常,异常处理程序捕获的异常太泛化将导致更多的错误,这不是编写优秀的代码所期望的。

注意:你能够使用异常的大的分类去处理一些异常。也能够指定特定异常去区分程序中的其他异常,让异常的类型更加的精细。

总结:程序中使用异常,说明了一个错误可能出现。为了抛出异常,可以使用 throw语句,然后跟随着一个异常对象(Throwable的子类为了提供抛出异常的特定的信息),抛出方法内部不捕获的、非检测性异常必须在方法的声明中加上 throws 语句。

程序能够使用 try  catch  finally 块捕获异常。
  • try块中包含了可能出现异常的代码块
  • catch块中代码叫做一个异常处理程序,能够处理特定的异常。
  • finally块是保证执行的代码块(前提是JVM没有其他的异常退出),finally块是关闭文件,恢复资源,以及清理别的资源。
try语句块可以至少附加一个catch块或者一个finally块和多个catch块。
异常类对象显示了抛出异常的类型,异常对象可以包含更多的异常信息,在异常链中,一个异常能够指向引起它的异常...

异常大的分类分为检测性异常(checked exception)和非检测性异常(unchecked exception)。其中检测性异常是指在编译器就能够发现的异常。

非检测性异常又包括Error 和 Exception  其中Error代表了非程序内部的异常,即来自于外部的异常:比如: 硬盘破坏、系统故障等。 Exception是指程序内部的异常:一般是指程序是逻辑出了问题,或者 不正确的使用了API造成的异常。

不管是检测性异常还是非检测性异常 都是继承自 Throwable类。


练习:
try {
    
} finally {
   
}
以上代码合法。
try {

} catch (Exception e) {
   
} catch (ArithmeticException a) {
    
}
以上代码错误,第一个捕获异常已经捕获了所有的异常,第二个就多余了,顺序颠倒一下是可以的。

a:int[] A  ; A[0]=0;  -- 编译错误,数组没有初始化,不能够通过编译。
b:JVM开始运行你的程序,但是JVM不能找到java平台的类 --- error
c:程序读流,到达流的末端, -------no  exception
d:在到达流的末端后和关闭流之前,程序试着再一次读取流中的信息, checked exception (检测性异常)










评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值