Java之异常

前言

  在理想状态下,用户输入数据的格式永远都是正确的,选择打开的文件也一定存在,并且永远不会有 Bug。
  但现实却往往很残酷,程序总会碰到不良的数据或在执行中出现 Bug 。。。

  程序犯错用户就很不爽,用户不爽就不用你写的程序了(小老弟,你咋回事呀)。
  为了避免这类事情发生,至少应保证程序做到下面几点:

  • 向用户报告错误
  • 保存当前所有的工作结果
  • 允许用户以妥善的形式退出程序

  对于该情况,Java 使用称为异常处理的错误捕获机制来处理。

Java 异常体系

  在 Java 中,异常对象都是派生于Throwable类的一个实例,层次结构图如下:

  所有的异常都是由Throwable继承而来,之后分为:

  • Error:描述了 Java 运行时系统的内部错误和资源耗尽错误,应用程序不该抛出这种类型的对象。
  • Exception:该层次结构又派生了
    • RuntimeException:由程序错误导致的异常
    • 其他异常(如IOException等等):程序本身没问题,但由于像 I/O 错误这类问题导致的异常。

  具体而言,派生于RuntimeException的异常包括以下几种情况:

  • 错误的类型转换
  • 数组访问越界
  • 访问null指针

  若出现RuntimeException异常,那么就一定是你的问题。所以我们应通过检测数组下标是否越界来避免ArrayIndexOutOfBoundsException异常,在使用变量之前检测其是否为null来杜绝NullPointerException异常。

  派生于其他异常的异常包括以下几种情况:

  • 试图打开一个不存在的文件
  • 试图在文件尾部之后读取数据
  • 试图根据给定的字符串查找 Class 对象,而该类并不存在

  如何处理不存在的文件呢?先检查其是否存在再打开它吗?该文件是否可能在被检查之前就被删除了呢?
  因此,是否存在取决于环境,而不仅仅是你的代码。

  从另一个角度来看,Java 语言规范将异常分为 2 类:

  • 非受查(unchecked)异常:派生于Error类或RuntimeException类的所有异常
  • 受查(checked)异常:所有其他的异常

  笔者简单理解,对受查异常,在编译期其会被编译器(如Eclipese、IDEA)检测到,要求你对异常情况做一定处理。如使用IO流读取文件时,文件可能不存在,编译器会要求你抛出一个 IO 异常。
  而非受查异常则无法在编译期被检测到,要求你自己判断。如发生数组越界的问题是无法被编译器判断的。
  总之, 一个方法必须声明所有可能抛出的受查异常, 而非受查异常要么不可控制(Error), 要么就应该避免发生(RuntimeException)。
  若方法没有声明所有可能发生的受查异常, 编译器就会发出一个错误消息。

抛出异常

  出现错误就不正常,那我们就希望抛出这个异常,然后做相应的(捕获)处理。
  那么,异常该如何抛出呢?
  异常抛出的步骤如下:

  • 1) 找到一个合适的异常类
  • 2) 创建这个类的一个对象
  • 3) 将对象抛出

  如throw new ArraylndexOutOfBoundsException

异常捕获

  抛出异常后,就要去抓住(捕获)它,不要让他跑啦!
  若某个异常发生的时候没有被捕获,那程序就会终止,并在控制台打印大量的异常信息。
  但是,我们只想先去打印所关心的异常内容,之后再考虑怎么解决它。
  这时就得先将异常捕获,然后再做相关处理:到底打印什么并决定给使用的用户展示什么信息?
  在 Java 中,捕获异常必须使用try/catch块:

1
2
3
4
5
try{
in.read();
}catch(IOException e){
e....//相关操作
}

  若在try语句块中的任何代码抛出了一个在catch字句中说明的异常类,那么:

  • 1) 程序将跳过try语句块的其余代码
  • 2) 程序将执行catch字句中的处理器代码

  若在try语句块中的中代码没有抛出异常,那么程序将跳过catch字句。
  若方法中的任何代码抛出了一个在catch字句中没有声明的异常类型,那么这个方法将立即退出,所以希望调用者为这种类型的异常设计下catch语句。
  当然捕获多个异常也是可以的,多加几个catch字句即可。
  当代码抛出一个异常时, 就会终止方法中剩余代码的处理, 并退出这个方法的执行。

  若方法获得了一些本地资源, 并且只有这个方法自己知道, 又若这些资源在退出方法之前必须被回收, 那么就会产生资源回收问题,我们可以通过finally字句很好地解决这个问题。代码如下:

1
2
3
4
5
6
7
try{
in.read();
}catch(IOException e){
e....//相关操作
}finally{
in.close();
}

  关于try catch finally的执行顺序可以参考这篇文章

补充:自定义异常的创建

  在程序中, 可能会遇到 Java 中所有异常类都无法描述的问题。
  此时, 可以创建自定义的异常类。
  下面为一个自定义异常的代码:

1
2
3
4
5
6
7
8
9
public class MyException extends RuntimeException{

public MyException() {
}

public MyException(String message) {
super(message);
}
}

  自定义异常时创建时需要注意以下几点:

  • 所有异常类都必须是 Throwable 的子类。
  • 若想自定义检查性异常类,需要继承 Exception 类。
  • 若想自定义运行时异常类,需要继承 RuntimeException 类。

  习惯上, 自定义异常类应该包含两个构造器, 一个是默认的构造器; 另一个是带有详细描述信息的构造器(父类 ThrowabletoString 方法将会打印出这些详细信息, 这在调试中非常有用)。

参考

  • Cay S. Horstmann. Java 核心技术卷一 [M]. 机械工业出版社,2016
0%