Throwable
假设在Java程序运行期间出现了一个错误,这个错误产生的原因可能是对象不存在、下标错误、网络原因等,有些是外部因素,有些是代码因素。当出现错误时,我们希望能够有一些相应的处理,要么给与提示,要么对错误进行处理,在Java中,是通过抛出和捕捉异常对象进行处理的。
Throwable是Java中所有异常对象的父类,它定义了获取错误信息、栈堆信息、生成异常对象等一系列公共方法,并实现了Serializable接口,可以被序列化。在方法中,当出现异常的时候,可以通过throw将相应的子类异常抛出。
常见的错误类型
- 用户输入错误
- 设备错误
- 物理限制
- 代码错误
Exception继承体系
在Java 程序设计语言中,异常对象都是派生于Throwable 类的一个实例,所有异常都是由Throwable继承而来,并且分为了Error和Exception。
根据Java中常见的Exception和Error做了一个继承体系图如下。在异常体系中包含受检结构和非受检结构。受检结构即为受检异常,非受检结构包含非受检异常和Error及其子类,非受检异常都是RuntimeException及其子类。
受检异常
受检异常的作用主要是告诉调用者可能出现什么异常,并要求调用者进行处理或者抛出。如果API的提供者希望调用者来对异常进行处理,那么就可以使用受检异常,通过抛出异常,强制让调用者try-catch,在catch中进行处理或者继续抛出。方法中每个抛出的异常,都是对调用者的一种潜在异常提示,提醒调用者可能发生的问题。如IOException,ClassNotFoundException等。
- 注意事项
- 对于可恢复的场景,要抛出受检异常,通知调用者进行处理
- 要在受检异常上提供方法,以便协助恢复
- 抛出受检异常的方法不能直接在Stream中使用
- 对外部提供API的时候,尽量不要使用受检异常,因为调用方需要通过try-catch捕捉处理或者抛出,try-catch模块同时也阻止了JVM本来可能要执行的某些特定优化。
- 如果打算选择其他方法代替受检异常。可以通过选择提供状态测试方法(如Iterator的hasNext方法)或者optional返回值或者可识别的返回值(null)来解决。如果有并发情况,不要使用状态测试方法,在测试方法执行前后期间对象的状态可能在别的线程被发生了变化。
- 消除受检异常最容易的方法是,返回所要的结果类型的一个optional。这种方法的缺点是,方法无法返回任何额外的信息,来详细说明它无法执行你想要的计算。相反,异常则具有描述性的类型,并且能够导出方法,以提供额外的信息。
未受检结构
非受检结构包括RuntimeException和Error。
如果程序抛出未受检的异常或者错误,往往就属于不可恢复的情形,继续执行下去有害无益。如果程序没有捕捉到这样的可抛出结构,那么将会导致当前线程中断,并且提示适当的错误信息,比如最常见的NullPointException
RuntimeException
如果出现了RuntimeException,那么一定是程序的问题。比如NullPointerException,这个异常不需要对外抛出,因为我们可以通过代码避免这种异常,因此不需要对外抛出。
- 对于程序错误,不能继续执行,要抛出运行时异常
- 不确定是否可恢复,则抛出未受检异常
- 一般情况下尽量使用JDK中已有的运行时异常,特殊情况下通过继承RuntimeException自定义新的异常
Error
Error 类层次结构描述了Java 运行时系统的内部错误和资源耗尽错误。应用程序不应该抛出这种类型的对象。如果出现了这样的内部错误,除了通告给用户,并尽力使程序安全地终止之外,再也无能为力了,如OutOfMemoryError错误,当抛出这个Error时,基本程序已经无法运行
- Error是不可控的
- 错误往往被JVM保留下来使用,以表明资源不足、约束失败,或者其他使程序无法继续执行的条件
- 最好不要再实现任何新的Error子类
捕获异常
如果某个异常发生的时候没有在任何地方进行捕获,那程序就会终止执行,并在控制台上打印出异常信息, 其中包括异常的类型和堆栈的内容。
要想捕获一个异常, 必须设置try/catch 语句块。如果在try模块中发生了一个异常,那么程序将跳过try模块中剩余代码,执行catch模块中的代码,如果没有发生异常,那么执行完try中代码后,跳过catch代码。
// 捕获异常
try {
System.out.println(1/0);
} catch (Exception e) {
e.printStackTrace();
}
// 捕获多个异常
try {
System.out.println(1/0);
} catch (NullPointerException | ArithmeticException e) {
e.printStackTrace();
}
转换异常
当捕获到一个异常,想把它转变成其他异常的时候,除了调用其他异常的构造方法,还可以通过使用Throwable中继承的方法initCause,它会产生新的异常并且记录导致这个异常的原因,通过getCause可以获取原因。
try {
System.out.println(1/0);
} catch (Exception e) {
RuntimeException e1 = new RuntimeException();
e1.initCause(e);
e1.printStackTrace();
}
finally
finally模块中的代码必定会执行。如果try模块中抛出了异常,并且异常被捕捉,执行顺序为try异常前代码-catch-finally; 如果try模块中未发生异常或者发生异常未被捕捉,那么执行顺序为try-finally。
try {
System.out.println(1/0);
} catch (Exception e) {
e.printStackTrace();
} finally {
System.out.println("finally");
}
System.out.println(1); // 会被执行
finally中的return 会覆盖try中的return
try {
return 1;
} catch (Exception e) {
e.printStackTrace();
} finally {
return 2; // 覆盖
}
因为finally中的语句必定会执行,因此finally中一般也会被用来释放资源,如InputStream和OutStream。
InputStream is = null;
try {
is = new FileInputStream(new File(""));
System.out.println(1/0);
} catch (FileNotFoundException e) {
e.printStackTrace();
} finally {
try {
is.close();
} catch (IOException e) {
// 释放资源如果抛出异常可能会覆盖上面的异常
}
}
如果实现了AutoCloseable接口,那么可以使用带资源的try模块方式,当出现异常或者正常执行结束,程序自动释放资源。
try (InputStream is = new FileInputStream(new File(""))){
System.out.println(1/0);
} catch (IOException e1) {
e1.printStackTrace();
} finally {
}
API
- initCause 生成新的异常对象
- getCause 获取异常原因
- getMessage 异常信息
- getStackTrace 获取异常栈堆信息
- printStackTrace 打印异常的堆栈信息
其他注意事项
- 子类不能抛出比父类更大的异常,比如父类抛出NullPointException,那么子类不能抛出RuntimeException
- 异常是可序列化的
- 只有针对异常时才使用异常,因为业务中使用异常比正常代码要慢得多
- 不要定义任何既不是受检异常也不是运行时异常的抛出类型。
- 只有在底层方法的规范碰巧可以保证“它所抛出的所有异常对于更高层也是合适的”情况下,才可以将异常从低层传播到高层
- 异常转译:更高层的实现应该捕获低层的异常,同时抛出可以按照高层抽象进行解释的异常