Java异常处理总结

前言

  这一讲抽取Java异常处理的核心内容与基本思想,作一个总结,既作为个人笔记,也可供读者参考。

基本思想

  • 降低错误处理代码的复杂度,让程序实际的逻辑实现与错误处理相分离,使代码的阅读,编写和调试工作更加井井有条。
  • 在能处理时捕捉异常,不能处理时则将异常传给其他地方处理。
  • 将异常分类,用异常类的名字或携带的信息标识异常类别与详细信息,一般只用名字标识即可。在处理时不针对异常抛出点处理,只对异常类型处理,简化了异常处理的代码。

异常对象的类型

必检异常与非必检异常

  我们指的异常类都是Exception类型或者继承了Exception类型的子类。这些异常类可以分为两大部分,一部分是必检异常,另一类是非必检异常。只有RuntimeException及其子类是非必检异常,其余都是必检异常。

  非必检异常对象由Java虚拟机自行管理,当异常发生时,会自动在堆中new一个异常对象。如果非必检异常始终不捕捉,则会沿着方法调用栈一直往上冒泡,直到控制台,并且自动调用prinStackTrace()方法输出异常信息。而必检异常必须手动在堆中创建,然后用关键字throw抛出,且必须在程序的某一处进行捕捉处理。如果不处理,则需在main方法里声明抛出异常,将异常抛给控制台,让异常信息在控制台输出。

RuntimeException的使用场景

  我的理解是,必检异常都是用户在当前环境下直接产生的异常。而RuntimeException及其子类则代表编程错误。所谓编程错误,有两种含义:

  1. 在程序员编程时应该进行检查的低级错误,如数组越界异常ArrayIndexOutOfBoundsException,空指针异常NullPointerException等,它们都是RuntimeException的子类。
  2. 某些间接的或者不是由方法调用者所能掌控的错误。例如别的地方传来的空指针引用,或者与该方法本身无直接关联的错误。

自定义异常

  所有继承了Exception或其子类的类,都是自定义异常类。我们可以根据自己的业务需要创建新的异常类。

异常对象的处理方式

抛出

  如果方法里获得了一个异常对象,但不知道怎么处理,或者在该方法的作用域内没有办法处理,则必须将异常对象往方法调用栈的上一级抛出,让别的地方处理。

  我们在设计方法的时候,必须声明该方法可能会抛出什么异常,在方法名的后面跟throws再加上可能抛出的异常类表示。所有可能抛出的必检异常都必须声明,且调用该方法时候要么在调用该方法的方法签名上再进行抛出声明,要么用try…catch语句捕捉处理异常。否则产生编译错误。

捕捉

  catch作用域就是异常处理程序代码所在地,对捕捉到的异常进行处理。如果同时捕捉多个异常,且这些异常存在继承关系,则必须将继承链里相对最后的子类异常放在前面,并按继承链的顺序由子类到父类依次排列。这是因为异常捕捉只要捕捉到相应类型的异常则终止,后面的捕捉块都不会起作用。而Java里支持多态,所以如果父类异常放前面,则子类异常永远不会被捕捉到。

无法处理的异常

  无法处理的异常需要抛出,但如果我们一直不知道如何处理该异常,那应该怎么办呢?特别对于必检异常,强制我们进行捕获或继续上抛,如果始终不捕捉,那么异常对象会怎么样呢?

空处理

  千万不能为了应付编译,捕捉了异常,而进行空处理。如:

1
2
3
4
5
try{
}catch(Exception e){
//do nothing
}

  这样的话,即便发生了异常,我们也无从得知程序有错,因为异常信息被吞没了。

输出异常信息

  即便为了应付编译,我们也得输出异常信息,这样我们可以定位错误,以便在需要的时候修改异常处理的代码。

1
2
3
4
5
try{
}catch(Exception e){
e.printStackTrace()
}

  Exception类继承自Throwable类,Throwable类有3个关于异常输出的方法:

  • printStackTrace()
  • printStackTrace(PrintStream)
  • printStackTrace(java.io.PrintWriter)

  printStackTrace()会把异常抛出点往上的所有方法调用栈信息输出到标准错误流。除了输出到标准错误流,也可以指定参数,指定输出到由PrintStream流或者PrintWriter包装的一个流。

只能部分处理异常

  对于某些异常,可能在当前方法内捕捉可以处理一部分问题,但还有其他问题需要由其他方法继续处理。这种情况,我们可以先捕捉,进行处理,然后重新在catch代码块里抛出。

1
2
3
4
5
6
try{
}catch(Exception e){
e.printStackTrace();
throw e;
}

  但重新抛出产生了一个问题,即重新抛出的异常仍然保留的是原来调用栈的信息,在别的地方再进行捕捉时,printStackTrace()方法打印的仍然是第一次抛出时的方法调用栈。如果需要更新调用栈信息,则需要用fillInStackTrace()方法。

1
2
3
4
5
6
try{
}catch(Exception e){
e.printStackTrace();
throw (Exception)e.fillInStackTrace();
}

  fillInStackTrace()方法会返回一个更新了调用栈信息的Throwable类型对象。

main方法抛出异常

  如果异常一直抛出到main方法仍然无法处理,那么可以声明main方法抛出异常,异常会直接传给控制台输出调用栈信息。

将必检异常转换为非必检异常

  我们可以用e.printStackTrace()的形式,在不知道怎么处理异常时捕捉异常并单纯输出异常信息。然而,捕捉就意味着处理,如果我们不处理只捕捉,则违背了异常处理的初衷。对于无法处理的异常,我们应始终抛出。而RuntimeException及其子类,都是非必检异常,可以自动沿调用链抛出直至控制台。我们可以利用这种特性,让必检异常转换为非必检异常,让它们抛出到控制台而始终不捕捉。等以后知道怎么处理之后,再修改代码。

异常链

  要了解异常类型转换,必须先介绍异常链。

  异常链的产生也是源自重新抛出。要在捕获一个异常后抛出另一个异常,并且希望把原始异常的信息保存下来,就被称为异常链。构造异常链,我们需要在堆里new一个新的异常对象,并且把原始异常对象传入该新的对象里。对于Error,Exception,RuntimeException,其构造函数就接收一个异常对象。但对于其他类型的异常,必须用initCause(Exception)方法传入异常对象。

  其后,我们可以捕捉新的异常对象,原始异常对象的信息也会保存在里面。

转换为RuntimeException

  了解了异常链,这种转换就非常简单了。

1
2
3
4
5
try{
}catch(IOException e){
throw new RuntimeException(e)
}

  以IOException为例,它是必检异常,通过传入RuntimeException的构造函数,就可以转换为非必检异常抛出了。

  我觉得对于暂时不知道怎么处理的异常,这种处理方式是最佳的方式。

资源清理

  Java只负责内存清理,所有与内存无关的清理工作,都需要手动清理,例如一些IO资源,数据库连接资源等。

  资源清理一般在finally代码块里实现。

  对于某些资源,如果在创建对象时就失败,就不需要清理了。可以用嵌套的try…catch语句实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
try{
BufferedReader in = new BufferedReader(new FileReader("Cleanup.java"));
try{
...
}catch(Exception e){
e.printStackTrace();
}finally{
try{
in.close();
}catch(IOException e){
e.printStackTrace();
}
}
}catch(Exception e){
e.printStackTrace();
}

  建议在所有创建资源对象的语句前后加上嵌套的try…catch语句。

  当然,我们也可以不用这种嵌套,但必须在每次清理之前判断资源对象是否为null。

  Java7还引入了一个新特性:try-with-resource。所有实现了java.lang.AutoCloseable的对象,都被视为资源。如果这些资源在try块内,则会在块结束之后自动被清理,不需要finally块,也不用显式地调用释放语句。

使用finally的问题

  • 使用finally容易造成异常丢失。如果在catch前出现了finally语句,且finally语句内又抛出了异常,那么原try块内的异常会被取代,不会被捕捉。前一个异常还未处理,又抛出新异常,原异常会丢失。
  • 如果在抛出异常时,finally内有return,则异常也会丢失。

异常处理的限制

  • 子类覆写的方法,抛出的异常必须在基类方法声明抛出的异常的范围内。
  • 子类的构造器方法声明抛出的异常,必须含有父类构造器方法声明的异常。因为子类构造器必然会调用父类的某个构造器,无论是显式还是隐式。

  这种限制,也是源于Java的多态机制。因为子类对象会被向上转型为基类对象,必须保证在使用基类时能捕获子类所有的异常。

总结

  要把异常处理好,需要很丰富的经验。以上只是异常处理的最基本知识。