前言
在理想状态下,用户输入数据的格式永远都是正确的,选择打开的文件也一定存在,并且永远不会有 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 | try{ |
若在try语句块中的任何代码抛出了一个在catch字句中说明的异常类,那么:
- 1) 程序将跳过
try语句块的其余代码 - 2) 程序将执行
catch字句中的处理器代码
若在try语句块中的中代码没有抛出异常,那么程序将跳过catch字句。
若方法中的任何代码抛出了一个在catch字句中没有声明的异常类型,那么这个方法将立即退出,所以希望调用者为这种类型的异常设计下catch语句。
当然捕获多个异常也是可以的,多加几个catch字句即可。
当代码抛出一个异常时, 就会终止方法中剩余代码的处理, 并退出这个方法的执行。
若方法获得了一些本地资源, 并且只有这个方法自己知道, 又若这些资源在退出方法之前必须被回收, 那么就会产生资源回收问题,我们可以通过finally字句很好地解决这个问题。代码如下:
1 | try{ |
关于try catch finally的执行顺序可以参考这篇文章
补充:自定义异常的创建
在程序中, 可能会遇到 Java 中所有异常类都无法描述的问题。
此时, 可以创建自定义的异常类。
下面为一个自定义异常的代码:1
2
3
4
5
6
7
8
9public class MyException extends RuntimeException{
public MyException() {
}
public MyException(String message) {
super(message);
}
}
自定义异常时创建时需要注意以下几点:
- 所有异常类都必须是
Throwable的子类。 - 若想自定义检查性异常类,需要继承
Exception类。 - 若想自定义运行时异常类,需要继承
RuntimeException类。
习惯上, 自定义异常类应该包含两个构造器, 一个是默认的构造器; 另一个是带有详细描述信息的构造器(父类 Throwable 的 toString 方法将会打印出这些详细信息, 这在调试中非常有用)。
参考
- Cay S. Horstmann. Java 核心技术卷一 [M]. 机械工业出版社,2016