第十五章 异常
最后更新时间:2020.05.19
本笔记大部分基于《on java 8》整理, 另外初学者一枚,大家多多关照,有错误可以在下面说出来 谢谢大家
为什么要有异常?
1.java 的基本理念是“结构不佳的代码不能运行”。
2.改进的错误恢复机制是提高代码健壮性的最强有力的方式。
3.java 的主要目标之一就是创建供他人使用的程序构件。
4.要想创建健壮的系统,它的每一个构件都必须是健壮的。
异常和普通问题的区别
**普通问题:**在当前环境下能得到足够的信息,总能处理这个错误。
**异常:**阻止当前方法或作用域继续执行的问题。因为在当前环境下无法获得必要的信息来解决问题。只能从当前环境跳出,并且把问题提交给上一级环境
抛出异常会发生什么
首先,同 Java 中其他对象的创建一样,将使用 new 在堆上创建异常对象。然后,当前的执行路径(它不能继续下去了)被终止,并且从当前环境中弹出对异常对象的引用。此时,异常处理机制接管程序,并开始寻找一个恰当的地方来继续执行程序。这个恰当的地方就是异常处理程序,它的任务是将程序从错误状态中恢复,以使程序能要么换一种方式运行,要么继续运行下去。
示例如下
if(t == null)
throw new NullPointerException();
如图:
t所接收的对象未曾初始化。
虚拟机在这会怎么做呢
在使用这个对象引用调用其方法之前,会先对引用进行检查。可以创建一个代表错误信息的对象,并且将它从当前环境中“抛出”,这样就把错误信息传播到了“更大”的环境中,它将在别的地方得到处理。(异常将在一个恰当的异常处理程序中得到解决,它的位置可能离异常被抛出的地方很远,也可能会跨越方法调用栈的许多层级。)
使用异常的优点
1.Java 使用异常来提供一致的错误报告模型,使得构件能够与客户端代码可靠地沟通问题。
2.Java 中的异常处理的目的在于通过使用少于目前数量的代码来简化大型、可靠的程序的生成,
3.异常处理是 Java 中唯一官方的错误报告机制,并且通过编译器强制执行
4.如果发生问题,它们将不允许程序沿着其正常的路径继续走下去
5.异常使得我们可以将每件事都当作一个事务来考虑,而异常可以看护着这些事务的底线
异常参数
而且我们总是new在堆上创建异常对象,这也会使用存储空间和调用构造器。
所有标准异常类都有两个构造器:一个是无参构造器;另一个是接受字符串作为参数,以便能把相关信息放入异常对象的构造器:
示例:
throw new NullPointerException("t = null");
在使用 new 创建了异常对象之后,此对象的引用将传给 throw。尽管异常对象的类型通常与方法设计的返回类型不同,但从效果上看,它就像是从方法“返回”的。可以简单地把异常处理看成一种不同的返回机制,
异常捕获
**解监控区域(guarded region):**它是一段可能产生异常的代码,并且后面跟着处理这些异常的代码。
如果在方法内部抛出了异常(或者在方法内部调用的其他方法抛出了异常),可以设置一个try块,用来捕获异常,也可以在在这个块里“尝试”各种(可能产生异常的)方法调用。
设置格式如下:
try {
//需要被捕获的代码块
}
异常处理程序
当然,抛出的异常必须在某处得到处理。这个“地点”就是异常处理程序,而且针对每个要捕获的异常,得准备相应的处理程序。异常处理程序紧跟在 try 块之后,以关键字 catch 表示:
try {
// 需要被捕获的代码块
} catch(Type1 id1) {
//被捕获的异常类型
} catch(Type2 id2) {
//被捕获的异常类型
} catch(Type3 id3) {
//被捕获的异常类型
}
每个 catch 子句(异常处理程序)看起来就像是接收且仅接收一个特殊类型的参数的方法。可以在处理程序的内部使用标识符(id1,id2 等等),这与方法参数的使用很相似。
异常处理程序必须紧跟在 try 块之后。当异常被抛出时,异常处理机制将负责搜寻参数与异常类型相匹配的第一个处理程序。然后进入 catch 子句执行,此时认为异常得到了处理。一旦 catch 子句结束,则处理程序的查找过程结束。 注意,只有匹配的 catch 子句才能得到执行
;
终止和恢复
异常处理理论上有两种基本模型:终止模型和恢复模型,( Java 和 C++所支持的模型是终止模型)。
自定义异常
不必拘泥于 Java 中已有的异常类型。Java 提供的异常体系不可能预见所有的希望加以报告的错误,所以可以自己定义异常类来表示程序中可能会遇到的特定问题。
要自己定义异常类,必须从已有的异常类继承,最好是选择意思相近的异常类继承(不过这样的异常并不容易找)。建立新的异常类型最简单的方法就是让编译器为你产生无参构造器,所以这几乎不用写多少代码:
class SimpleException extends Exception {}
public class InheritingExceptions {
public void f() throws SimpleException {
System.out.println(
"Throw SimpleException from f()");
throw new SimpleException();
}
public static void main(String[] args) {
InheritingExceptions sed =
new InheritingExceptions();
try {
sed.f();
} catch(SimpleException e) {
System.out.println("Caught it!");
}
}
}
输出为:
Throw SimpleException from f()
Caught it!
不过也许使用System.err
会更好一点,它不会随System.out
一起被重定向,这样更容易被用户注意,不是吗?
异常声明
Java 鼓励人们把方法可能会抛出的异常告知使用此方法的客户端程序员。这是种优雅的做法,它使得调用者能确切知道写什么样的代码可以捕获所有潜在的异常。当然,如果提供了源代码,客户端程序员可以在源代码中查找 throw 语句来获知相关信息,然而程序库通常并不与源代码一起发布。为了预防这样的问题,Java 提供了相应的语法(并强制使用这个语法),使你能以礼貌的方式告知客户端程序员某个方法可能会抛出的异常类型,然后客户端程序员就可以进行相应的处理。这就是异常说明,它属于方法声明的一部分,紧跟在形式参数列表之后。
异常说明使用了附加的关键字 throws,后面接一个所有潜在异常类型的列表,所以方法定义可能看起来像这样:
void f() throws TooBig, TooSmall, DivZero { // ...
但是,要是这样写:
void f() { // ...
就表示此方法不会抛出任何异常(除了从 RuntimeException 继承的异常,它们可以在没有异常说明的情况下被抛出,这些将在后面进行讨论)。
代码必须与异常说明保持一致。如果方法里的代码产生了异常却没有进行处理,编译器会发现这个问题并提醒你:要么处理这个异常,要么就在异常说明中表明此方法将产生异常。通过这种自顶向下强制执行的异常说明机制,Java 在编译时就可以保证一定水平的异常正确性。
不过还是有个能“作弊”的地方:可以声明方法将抛出异常,实际上却不抛出。编译器相信了这个声明,并强制此方法的用户像真的抛出异常那样使用这个方法。这样做的好处是,为异常先占个位子,以后就可以抛出这种异常而不用修改已有的代码。在定义抽象基类和接口时这种能力很重要,这样派生类或接口实现就能够抛出这些预先声明的异常。
这种在编译时被强制检查的异常称为被检查的异常。