一、为什么要抛出异常
如果某个方法不能够正确的采用正常的途径完整它的任务,就可以通过另一个路径退出方法。在这种情况下,方法不返回任何值,而是抛出一个封装了错误信息的对象。
注意:这个方法会立即退出,并不返回任何值。此外,调用这个方法的代码也将无法继续执行,取而代之的是,异常处理机制开始搜索能够处理这种异常状况的异常处理器
二、异常分类
- 所有的异常都是有Throwable继承而来,下一层分为两个分支 Error 和 Exception
- Error类层次结构描述了java运行时系统的内部错误和资源耗尽错误。应用程序不应该抛出这种类型的对象。
- Exception分为两个分支,一个分支派生于RuntimeException;另一个分支包含其他异常。划分规则,有程序错误导致的异常属于RuntimeException;而程序本身没问题,但由于像I/O错误这类问题导致的异常属于其他异常
- 派生于RuntimeException的异常包含以下几种情况:
- 错误的类型转换
- 数组访问越界
- 访问空指针
- 不是派生于RuntimeException的异常包括:
- 试图打开一个不存在的文件
- 视图在文件尾部后面读取数据
- 试图根据给定的字符串查找Class对象,,而这个字符串表示类并不存在
- 派生于RuntimeException的异常包含以下几种情况:
- 如果出现RuntimeException异常,那么一定是你的问题
- 异常又分为可检查(checked)异常和不检查(unchecked)异常,java语言规范将派生于RuntimeException和Error类的所有异常称为非检查异常,可检查异常在源代码里必须显式地进行捕获处理
三、自定义异常
有的时候,我们会根据需要自定义异常,这个时候除了保证提供足够的信息,还有两点需要考虑:
1. 是否需要定义成 Checked Exception,因为这种类型设计的初衷更是为了从异常情况恢复,作为异常设计者,我们往往有充足信息进行分类。
2. 在保证诊断信息足够的同时,也要考虑避免包含敏感信息,因为那样可能导致潜在的安全问题。如果我们看 Java 的标准类库,你可能注意到类似java.net.ConnectException,出错信息是类似“ Connection refused (Connection refused)”,而不包含具体的机器名、IP、端口等,一个重要考量就是信息安全。类似的情况在日志中也有,比如,用户数据一般是不可以输出到日志里面去的
四、抛出和捕获异常
1. 如果在子类中覆盖了超类的一个方法,子类方法中声明的受检查异常不能比超类声明的异常更通用。如果父类方法没用抛出任何受查异常,子类也不能任何受查异常。Java中,没有throws说明符的方法将不能抛出任何受查异常。
2. 尽量不要捕获类似 Exception 这样的通用异常,而是应应该捕获特定异常,在这里是 Thread.sleep() 抛出的 InterruptedException。
3. 不要生吞(swallow)异常。这是异常处理中要特别注意的事情,因为很可能会导致非常难以诊断的诡异情况。 生吞异常,往往是基于假设这段代码可能不会发生,或者感觉忽略异常是无所谓的。
如果我们不把异常抛出来,或者也没有输出到日志(Logger)之类,程序可能在后续代码以不可控的方式结束。没人能够轻易判断究竟是哪里抛出了异常,以及是什么原因产生了异常。
4. 带资源的try语句
a) finally子句
执行finally语句块时,调用close方法。而close方法本身也有可能抛出像IOException异常。当出现这种情况,原始的异常将丢失,转而抛出close方法的异常
b) try-with-resources
try(Resourse res = . . .){
work with res
}
带资源的try语句可以很好的处理上面那种情况。原来的异常会重新抛出,而close方法抛出的异常会被抑制。
五、使用异常执行技巧
1. 异常处理不能代替简单的测试
捕获异常所花费的时间是巨大的,因此异常处理的基本规则是:只有在异常情况下使用异常机制。
2. 不要过分地细化异常
3. 利用异常层次结构
a) 不要只抛出RuntimeException异常,应该寻找更适合的子类或创建自己的异常类
b) 不要只捕获Throwable异常,否则会使代码更难读、更难维护
c) 考虑受查异常与非受查异常得而区别。已检查异常本来就非常庞大,不要为逻辑错误抛出这些异常。(例如,反射库的做法就不正确。调用者却经常需要捕获那些早已知道不可能发生的异常)
4. 早抛出
例如,当栈空是,执行出栈操作是返回一个null还是抛出一个异常,通常,在有错的地方抛出一个EmptyStackException异常比在后面抛出一个NullPointException异常更好。
5 晚捕获 传递异常要比捕获异常要更好。
六、从性能角度来审视一下 Java 的异常处理机制
1. try-catch 代码段会产生额外的性能开销,或者换个角度说,它往往会影响 JVM 对代码进行优化,所以建议仅捕获有必要的代码段,尽量不要一个大的 try 包住整段的代码;与此同时,利用异常控制代码流程,也不是一个好主意,远比我们通常意义上的条件语句(if/else、switch)要低效。
2. Java 每实例化一个 Exception,都会对当时的栈进行快照,这是一个相对比较重的操作。如果发生的非常频繁,这个开销可就不能被忽略了。
七、思考题
1. 请对比 Exception 和 Error,另外,运行时异常与一般异常有什么区别?
答:Exception 和 Error 都是继承了 Throwable 类,在 Java 中只有 Throwable 类型的实例才可以被抛出(throw)或者捕获(catch),它是异常处理机制的基本组成类型。
Exception 和 Error 体现了Java 平台设计者对不同异常情况的分类。Exception 是程序正常运行中,可以预料的意外情况,可能并且应该被捕获,进行相应处理。
Error 是指在正常情况下,不大可能出现的情况,绝大部分的Error 都会导致程序(比如 JVM 自身)处于非正常的、不可恢复状态。既然是非正常情况,所以不便于也不需要捕获,常见的比如 OutOfMemoryError 之类,都是 Error 的子类。 Exception 又分为可检查(checked)异常和不检查(unchecked)异常,可检查异常在源代码里必须显式地进行捕获处理,这是编译期检查的一部分。
不检查异常就是所谓的运行时异常,类似 NullPointerException、ArrayIndexOutOfBoundsException之类,通常是可以编码避免的逻辑错误,具体根据需要来判断是否需要来判断是否需要捕获,并不会在编译期强制要求
2. NoClassDefFoundError 和ClassNotFoundException 有什么区别。
答:NoClassDefFoundError是一个错误(Error),而ClassNOtFoundException是一个异常,在Java中对于错误和异常的处理是不同的,我们可以从异常中恢复程序但却不应该尝试从错误中恢复程序。
ClassNotFoundException的产生原因:
Java支持使用Class.forName方法来动态地加载类,任意一个类的类名如果被作为参数传递给这个方法都将导致该类被加载到JVM内存中,如果这个类在类路径中没有被找到,那么此时就会在运行时抛出ClassNotFoundException异常。
ClassNotFoundException的产生原因主要是:
Java支持使用反射方式在运行时动态加载类,例如使用Class.forName方法来动态地加载类时,可以将类名作为参数传递给上述方法从而将指定类加载到JVM内存中,如果这个类在类路径中没有被找到,那么此时就会在运行时抛出ClassNotFoundException异常。
解决该问题需要确保所需的类连同它依赖的包存在于类路径中,常见问题在于类名书写错误。
另外还有一个导致ClassNotFoundException的原因就是:当一个类已经某个类加载器加载到内存中了,此时另一个类加载器又尝试着动态地从同一个包中加载这个类。通过控制动态类加载过程,可以避免上述情况发生。
NoClassDefFoundError产生的原因在于:
如果JVM或者ClassLoader实例尝试加载(可以通过正常的方法调用,也可能是使用new来创建新的对象)类的时候却找不到类的定义。要查找的类在编译的时候是存在的,运行的时候却找不到了。这个时候就会导致NoClassDefFoundError.
造成该问题的原因可能是打包过程漏掉了部分类,或者jar包出现损坏或者篡改。解决这个问题的办法是查找那些在开发期间存在于类路径下但在运行期间却不在类路径下的类。