[Java开发之路](5)异常详解
最后更新于:2022-04-01 09:59:34
### 1. 异常分类
在Java程序设计语言中,异常对象都是派生于Throwable类的一个实例。其是如果Java中的异常类不能满足需求,用户可以创建自己的异常类。
下图是Java异常层次结构的一个简化示意图。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-18_56eba2a75b56e.jpg)
从图上可以看出,所有的异常都是继承于Throwable类,但是在下一层立即分解为两个分支:Error和Exception。
(1)Error
Error描述了Java运行时系统的内部错误和资源耗尽错误。应用程序不应该抛出这种类型的错误,如果出现了这样的内部错误,除了通告用户,并尽力使程序安全的终止之外,再也无能为力了。这种情况很少见。大多数错误与代码编写者执行的操作无关,而表示代码运行时 JVM出现的问题。例如,Java虚拟机运行错误(Virtual MachineError),当 JVM 不再有继续执行操作所需的内存资源时,将出现 OutOfMemoryError。这些异常发生时,Java虚拟机(JVM)一般会选择线程终止。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-18_56eba2a771785.jpg)
这些错误表示故障发生于虚拟机自身、或者发生在虚拟机试图执行应用时,并且这些错误是不可查的,因为它们在应用程序的控制和处理能力之 外,而且绝大多数是程序运行时不允许出现的状况。对于设计合理的应用程序来说,即使确实发生了错误,本质上也不应该试图去处理它所引起的异常状况。
(2)Exception
程序设计者应该关注的是Exception,这一层次异常又分为两个分支:IOException和RuntimeException。划分这两个分支的规则是:由程序错误导致的异常属于RuntimeException;而程序本身没有问题,但是由于IO错误这类问题导致的异常属于IOException。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-18_56eba2a784038.jpg)
派生于RuntimeException的异常包括:
- 异常的运算条件,如一个整数除以0时
- 错误的类型转换
- 数组访问越界
- 访问空指针
派生于IOException的异常包括:
- 试图在文件尾部后面读取数据
- 试图打开一个不存在的文件
“如果出现RuntimeException异常则表明一定是你的问题”,这是一条相当有道理的规则。
<table cellspacing="0" cellpadding="0" style="border-collapse:collapse; border:1px solid rgb(187,187,187); width:1042px"><tbody><tr><td style="border-collapse:collapse; border:1px solid rgb(187,187,187)"><br/> 注意:<br/> 异常和错误的区别:异常能被程序本身可以处理,错误是无法处理。</td></tr></tbody></table>
(3)Checked Exception 与 UnChecked Exception
Java语言规范将派生于Error类或者RuntimeException类的所有异常称为未检查异常(UnChecked 异常),所有其他的异常(包括IOException)称为已检查异常(Checked 异常)。编译器将核查是否为所有的Checked 异常提供了异常处理器。
### 2.声明已检查异常
如果遇到了无法处理的情况,那么Java的方法可以抛出一个异常。这个道理很简单:一个方法不仅需要告诉编译器将要返回什么值,还要告诉编译器有可能发生什么错误。
例如:一段读取文件的代码知道优肯风读取的文件不存在,或者内容为空,因此,试图处理文件信息的代码就需要通知编译器可能会抛出IOException异常。
方法应该声明所有可能抛出的已检查异常,这样可以反映出这个方法可能抛出哪类已检查异常。
例如:下面是标准类库中提供的FileInputStream类的一个构造方法的声明异常情况:
~~~
public FileInputStream(String name) throws FileNotFoundException
~~~
这个异常声明表示这个构造方法根据给定的字符串name正确情况下产生一个FileInputStream对象,但是也有可能抛出一个FileNotFoundException异常。如果抛出异常,方法不会初始化一个FileInputStream对象,而是抛出一个FileNotFoundException对象。抛出异常之后,运行时系统开始搜索异常处理器,以便知道如何处理 FileNotFoundException对象。
不是所有可能抛出的异常都必须进行声明,以下4种情况时记得抛出异常:
- 调用一个抛出已检查异常的方法,如FileInputStream构造方法
- 程序运行过程中发现错误,并且利用throw语句抛出一个已检查异常
- 程序出现错误,例如,a[-1]=0会抛出一个下标越界的未检查异常
- Java虚拟机和运行库出现的内部错误
对于前两种情况必须进行声明。
不需要声明Java的内部错误,即从Error继承的错误,任何程序代码都具有抛出那些异常的潜能,但是它们在我们的控制范围之外。同样也不应该声明从RuntimeException继承的那些未检查异常。
~~~
void Read(int index) throws ArrayIndexOutOfBoundsException // bad style
{
...
}
~~~
这些运行时错误完全在我们的控制范围之内,如果特别关注数组下标引发的错误,就会将更多的时间花费在修正程序中的错误上,而不是说明这些错误发生的可能性上。
总结:
<table cellspacing="0" cellpadding="0" style="border-collapse:collapse; border:1px solid rgb(187,187,187); width:1042px"><tbody><tr><td style="border-collapse:collapse; border:1px solid rgb(187,187,187); width:1041px"><span style="color:#ff0000"><br/></span><ol><li><span style="color:#ff0000; font-size:10.5pt; line-height:1.5">一个方法必须声明所有可能抛出的已检查异常,而未检查异常要么不可控制(Error),要么就应该避免发生(RuntimeException)。</span><br/></li><li><span style="color:rgb(255,0,0); font-size:10.5pt; line-height:1.5"> 如果一个方法没有声明所有可能发生的已检查异常,编译器就会给出一个错误信息。</span><br/></li><li><span style="color:rgb(255,0,0); font-size:10.5pt; line-height:1.5">如果类中的一个方法声明会抛出一个异常,而这个异常是某个特定类的实例时,则这个方法就有可能抛出一个这个类,或者这个类一个子类的异常。</span></li></ol></td></tr></tbody></table>
### 3.如何抛出异常
假设有一个方法用来读取文件内容,给定的文本长度为1024,但是读到700个字符之后文件就结束了,我们认定这不是一种正常的情况,希望抛出异常。
首先决定应该抛出什么类型的异常(EOFException),知道之后抛出异常的语句如下:
~~~
// 第一种方法
throw new EOFException();
// 第二种方法
EOFException e = new EOFException();
throw e;
~~~
EOFException类还有一个含有一个字符串参数的构造方法,可以更加细致描述异常出现的状况:
~~~
String gripe = "未到指定长度,文件读取结束";
throw new EOFException(gripe);
~~~
对于一个已经存在的异常类,抛出异常过程:
- 找到一个合适的异常类
- 创建这个异常类的一个对象
- 将对象抛出
### 4.创建异常类
在程序中,可能会遇到任何标准程序类都没有能够充分描述清楚的问题,这种情况下,我们需要创建我们自己的异常类。我们需要做的就是定义一个派生类于Exception,或者派生于Exception子类的类。习惯上,定义的类应该包含两个构造器,一个是默认的构造器,一个是带有详细描述信息的构造器。
~~~
public class FileFormatException extends Exception{
/**
*
*/
private static final long serialVersionUID = 1L;
// 默认构造器
public FileFormatException(){
}
// 带有详细描述信息的构造器
public FileFormatException(String gripe){
super(gripe);
}
}
~~~
现在我们可以抛出我们自己定义的异常类型了。
~~~
throw new FileFormatException;
~~~
### 5.捕获异常
如果某个异常发生的时候没有任何地方进行捕获,那程序就会终止,并在控制台上打印出异常信息,其中包括异常的类型和堆栈的内容。
~~~
package com.qunar.test;
public class ExceptionTest {
public static void main(String[] args) {
int a = 10;
int b = 0;
System.out.printf("%d / %d = %d",a,b,a/b);
System.out.println("测试结束...");
}
}
~~~
控制台信息:
Exception in thread "main" java.lang.ArithmeticException: / by zero
at com.qunar.test.ExceptionTest.main(ExceptionTest.java:8)
从异常信息可以看出程序并没有运行完全,没有输出“测试结束...”,程序就终止,并且在控制台打印出异常信息。
要想捕获异常,必须使用try/catch语句块。
~~~
try{
code
more code
more code
}
catch (Exception e) {
handle for this type
}
~~~
(1)如果在try语句块中任何代码抛出一个在catch子句中说明的异常类,那么:
- 程序将跳过try语句块的剩余代码
- 程序将执行catch子句的处理器代码
(2)如果在try语句块中代码没有抛出异常,那么程序将跳过catch子句。
(3)如果方法中的任何代码抛出了一个在catch子句没有声明的异常类型,那么这个方法就会立刻退出。
~~~
package com.qunar.test;
public class ExceptionTest {
public static void main(String[] args) {
int a = 10;
int b = 0;
try{
System.out.printf("%d / %d = %d",a,b,a/b);
}
catch (ArithmeticException e) {
System.out.println("a / b b 不能等于0");
}
System.out.println("测试结束...");
}
}
~~~
运行结果:
a / b b 不能等于0
测试结束...
看一个例子:(读取文本程序代码)
~~~
public void read(String name){
try{
InputStream inputStream = new FileInputStream(name);
int b;
while((b = inputStream.read()) != -1){
// ...
}//while
}
catch (IOException e) {
e.printStackTrace();
}
}
~~~
对于一个普通的程序来说,这样的处理异常基本上合乎情理,但是,通常最好的情况是什么也不做,而是将异常传递给调用者。如果read方法出现了错误,就让read方法的调用者去处理,如果采用这种处理方式,就必须声明这个方法可能会抛出一个IOException。
~~~
public void read(String name) throws IOException{
InputStream inputStream = new FileInputStream(name);
int b;
while((b = inputStream.read()) != -1){
// ...
}//while
}
~~~
如果调用了一个抛出已检查异常的方法,就必须对它进行处理或者将它继续进行传递。
出现了两种处理方式,那到底哪种方式更好呢?
通常,应该捕获那些知道如何处理的异常,而将那些不知道怎么处理的异常继续进行传递。如果想传递一个异常,就必须在方法添加一个throws说明符。仔细阅读Java API文档,以便知道每个方法可能会抛出哪种异常,然后再决定是自己处理,还是加到throws列表中。
### 6.捕获多个异常
在一个try语句块中可以捕获多个异常类型,并对不同类型的异常做出不同的处理。
~~~
try{
}
catch (FileNotFoundException e) {
// emergency action for missing files
}
catch (UnknownException e) {
// emergency action for unknown hosts
}
catch (IOException e) {
// emergency action for all other I/O problems
}
~~~
### 7.再次抛出异常与异常链
在catch子句中可以抛出一个异常,这样做的目的是改变异常的类型。如果开发了一个供其他程序员使用的子系统,那么用于表示系统故障的异常类型会有多种解释。ServletException就是这样一个异常类型的例子。执行Servlet的代码可能不想知道发生的错误的细节原因,但希望知道servlet是否有问题。
下面给出了捕获异常并将它再次抛出的基本方法:
~~~
try{
access the database
}
catch(SQLException e){// 发生错误的细节原因
throw new ServletException("database error:"+e.getMessage());// 只希望知道servlet是否有问题
}
~~~
再给出一个更好的方法:包装技术
~~~
try{
access the databse
}
catch(SQLException e){
Throwable se = new ServletException("database error");
// 设置初始异常
se.initCause(e);
throw se;
}
~~~
这个方法更好的地方在于当捕获到异常时可以使用下面语句得到原始异常(不会丢失原始异常的细节):
~~~
Throwable e = se.getCause();
~~~
这种方法还可以解决一下问题:
如果一个方法中发生了一个已检查异常,而不允许抛出它,那么包装技术就十分有用,我们可以捕获这个已检查异常,并把它包装成一个运行时异常。
### 8.finally子句
当代码抛出一个异常时,就会终止方法中剩余代码的处理,并退出这个方法的执行。如果方法获得了一些本地资源,并且只有这个方法知道,同时这些资源再退出之前必须回收。这就需要finally子句来解决。不管是否有异常,finally子句的代码都被执行。
举个例子,当发生异常时,恰当的关闭所有数据库的链接是非常重要的,这种情况就可以使用finally。
~~~
try{
}
catch (Exception e) {
}
finally{
}
~~~
(1)如果代码没有抛出异常。首先执行try语句块中的全部代码,然后执行finally子句中的代码。
(2)如果代码抛出异常,并且在catch子句可以捕获到。首先执行try语句块汇总的所有代码,直到发生异常为止,此时跳过try语句块中剩余代码,去执行与该异常匹配的catch子句中的代码,最后执行finally子句中的代码。
(3)如果代码抛出异常,但这个异常不是由catch子句捕获的。首先执行try语句块汇总的所有代码,直到发生异常为止,此时跳过try语句块中剩余代码,然后执行finally子句中的代码,并将异常抛给这个方法的调用者。
建议独立使用try/catch和try/finally语句块。这样可以提高代码的清晰度。例如:
~~~
package com.qunar.test;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
public class ExceptionTest {
public static void main(String[] args) {
String name = "";
InputStream in = null;
// 确保报告出现的错误
try{
in = new FileInputStream(name);
// 确保关闭输入流
try{
// code that might throw exceptions
}
finally{
in.close();
}
}
catch (IOException e) {
// show error message
}
}
}
~~~
内层的try语句块只有一个职责,就是确保关闭输入流。外层的try语句块也只有一个职责,就是确保报告出现的错误。这种设计不仅清楚,而且还具有一个功能,就是将会报告finally子句中出现的错误。
当finally子句包含return语句时,会出现意想不到的结果。假设利用return语句从try语句块中退出。在方法返回前,finally子句的内容将被执行。如果finally子句中也有一个return语句,这个返回值将会覆盖原始的返回值。
~~~
package com.qunar.test;
public class ExceptionTest {
public static int function(int n){
try{
int r = n * n;
return r;
}
finally{
if(n == 2){
return 0;
}//if
}//finally
}
public static void main(String[] args) {
System.out.println(function(2));
}
}
~~~
如果调用function(2),那么try语句块中计算结果为4,并执行return语句。但是在方法真正返回前,还要执行finally子句,将使得方法返回0,覆盖了原始的返回值4。
有时候,finally也会有麻烦。
~~~
InputStream in = ...;
try{
// code that might throw exceptions
}
finally{
in.close();
}
~~~
假设在try语句块抛出了一个非IOException的异常,这个异常只有方法的调用者才能处理。执行finally子句,并调用in.close()方法,而close方法本身也可能抛出IOException异常。当这种情况出现时,原始的异常将会丢失,转而抛出clsoe方法的异常。