第2讲 Exception和Error有什么区别?
不存在永远不会出错的程序,异常是时有发生的。Java语言在设计之初就提供了相对完善的异常处理机制,这种机制大大降低了编写和维护可靠程序的门槛。今天的问题是Exception和Error有什么区别?
典型回答
Exception和Error都继承了Throwable类,在Java中只有Throwable类的实例才可以被抛出(throw)或者捕获(catch),它是异常处理机制的基本组成类型。
Exception和Error体现了Java平台设计者对不同异常情况的分类。
- Exception是程序正常运行中,可预料的意外情况,应该被捕获并进行相应处理。
- Error是正常情况下不太可能出现的情况,大部分Error都会导致程序处于非正常的、不可恢复的状态,也就是说不需要捕获,常见的如OOMError,是Error的子类。 Exception分为可检查(checked)异常和不检查(unchecked)异常。
- 可检查异常要求在源代码里进行显示地捕获处理,属于编译期。
- 不检查异常就是运行时异常,通常是编码避免的逻辑错误,需要根据需要判断时候进行捕获,不会在编译期强制要求。
考点分析
第一,要理解Throwable、Exception、Error的设计和分类。比如,掌握常见的子类,以及如何自定义异常等。
第二,要理解Throwable的元素和实践,掌握最基本的语法,如try-catch-finally,throw,throws关键字等。
知识拓展
举一个具体的例子,下面的代码有哪些不当之处?
try{
//业务代码
Thread.sleep(1000L);
}catch(Exception e){
//Ignore it
}
这段代码违反了异常处理的两个基本原则:
第一,不要捕获类似Exception这样的通用异常,而应该捕获特定异常,在这里应该是Thread.sleep()抛出的InterruptedException。
在日常的开发过程中,读代码是远远大于写代码的,Exception不能直观的体现出我们需要捕获的异常。
第二,不能生吞(swallow)异常,这样可能导致难以诊断的情况,生吞异常往往基于这段代码不会发生或者感觉是无所谓的,这在开发中是不能做这种假设的,如果不把异常抛出,或者输出到日志(logger),程序可能在后续以不可控的方式结束,而且不能判断哪里出了问题,什么原因导致的异常。
再看一个例子,下面的代码有哪些不当之处?
try{
//业务代码
//...
}catch(IOException e){
e.printStackTrace();
}
这段代码作为实验来说没问题,但是在产品代码是不允许这样写的。在稍微复杂一点的生产系统中,标准出错不是一个合适的输出选项,这种情况难以判断到底输出到哪里去了。
尤其对于分布式系统,如果发生异常,但是无法找到堆栈轨迹(stacktrace),纯属是诊断设置障碍。最好使用产品日志,详细地输出到日志系统里。
再看一个例子,体会Throw early,catch late的原则。 print void readPreferences(String fileName){ //...perform operations... InputStream in = new FileInputStream(fileName); //...read the preferences file... }
如果fileName是null,那么程序抛出NullPointerException,但是由于没有第一时间暴露出问题,堆栈信息难以理解,需要复杂的定位。这里修改一下,让问题“throw early”,对应的异常信息就非常直观。
public void readPreferences(String filename) {
Objects. requireNonNull(filename);
//...perform other operations...
InputStream in = new FileInputStream(filename);
//...read the preferences file...
}
至于"catch late",捕获异常后如何处理呢,最差的方式就是前面提到的“生吞异常”。本质上就是遮盖问题。如果实在不知道怎么处理,可以选择保留原有异常的cause信息,直接再抛出或者构建新的异常出去。在更高层面,往往更清楚合适的处理方式是什么。
有时我们会选择自定义异常,这种情况除了要保证提供足够的信息,还要考虑两点:
- 是否需要定义Checked Exception,便于从异常情况恢复。
- 在保证诊断信息足够的同时,要避免包含敏感信息,可能会导致安全问题。
最后从性能角度来审视一下JAVA的异常处理机制: try-catch代码会产生额外的性能开销,也就是说会影响JVM对代码的优化,所以建议仅捕获有必要的代码段,而不是一个try包住大段的代码。此外,使用异常控制代码流程比条件语句(if/else、switch)要低效。 JAVA每实例化一个Exception,都要对当时的栈进行快照,这是一个比较重的操作,如果发生频繁,那么开销不可忽略。
当我们的服务出现反应慢,吞吐量下降的时候,检查发生最频繁的Exception也是一种思路,诊断后台变慢的问题会放在后面的Java性能模块中系统探讨。