世界上并不存在不会出错的系统,只要是软件系统就一定会在运行的过程中出现开发人员无法预料的错误。如何处理意外发生就是我们作为一名开发人员所必须深入思考的问题。
Java语言提供了完善的异常处理机制,它有效的降低了编写以及维护的门槛(这也是Java语言大行其道的原因,好上手,机制全)。今天在这里和大家分享一下Java异常机制的特点以及应用。
现在我们回想一下,在日常开发中我们常常用到try……catch……语句,比如写一个文件输入输出流,此时我们必须catch相应的IOException、FileNotFountException,否则编译不通过,这里catch的是什么东西?答案是“Exception”。平时我们在系统运行中发现系统崩溃,比如OutOfMemoryError错误,这个OOM是什么东西?答案是“Error”。综上所述,Java中的异常分为两大类,Exception和Error,它们都继承了Throwable类(只有集成了Throwable类才能被抛出或者捕获)。
一、Exception与Error
Error是指正常情况下不大可能出现的情况,一旦出现很有可能导致程序处于不可恢复的非正常状态,开发人员不便于也不需要捕获。Exception则可以分为checked和unchecked异常,checked表示必须显示捕获处理,否则编译不通过。
Object | Object |
---|---|
Throwable | Throwable |
Error | Exception |
LinkageError、VirtualMachineError | unchecked、checked |
NoClassDefFoundError和ClassNotFoundException有什么区别?
NoClassDefFoundError是一个错误(Error),而ClassNotFoundException是一个异常(Exception),我们应该尝试从异常中恢复程序,而不应该尝试从错误中恢复程序。
ClassNotFoundException
ClassNotFoundException是一个checkedException,需要显示捕获处理。
ClassNotFoundException产生的原因是:Java使用Class.forName方法动态加载类到JVM内存中,但是如果传递的类在类路径中没有被找到,那么就会抛出ClassNotFoundException异常。ClassLoader.loadClass,ClassLoader.findSystemClass等方法在动态加载类到内存中的时候也可能会跑出去这个异常。
还有一种导致ClassNotFoundException发生的原因,当一个类已经被某个类加载器加载到内存中了,此时另一个类加载器又尝试动态从同一个包中加载这个类。此时也会出现ClassNotFoundException异常。
Demo:
public class Example {
public static void main(String args[]) {
try
{
Class.forName("GeeksForGeeks");
}
catch (ClassNotFoundException ex)
{
ex.printStackTrace();
}
}
}
NoClassDefFoundError
NoClassDefFoundError产生的原因是:JVM或者ClassLoader实例尝试加载类的时候找不到类的定义。它是一个LlinkageError,LinkageError发生的情况是在一个类依赖另一个类,而后者在前者编译后又发生了改变。导致出现LinkageError的错误。
Demo:
class GeeksForGeeks
{
void greeting()
{
System.out.println("hello!");
}
}
class G4G {
public static void main(String args[])
{
GeeksForGeeks geeks = new GeeksForGeeks();
geeks.greeting();
}
}
分别使用javac编译两个文件,然后使用java G4G运行。当我们把编译后的GeeksForGeeks.class文件拿走后,就会报NoClassDefFoundError的错误。
二、异常处理原则
通过上面的描述我们理解了Error和Exception之间的区别,接下来我们需要理解Java语言是如何操作Throwable元素。基本的语法包括“try-catch-finally”,“throw”,“throws”,“try-with-resources”等。异常处理有几个原则需要遵守:
(1)尽量不要捕获类似Exception这样的通用异常,而应该捕获特定异常。
(2)不要生吞异常。注意不要在生产环境使用e.printStackTrace()打印异常,最好使用产品日志,详细输出到日志系统。
(3)Throw early, catch late。提前把异常抛出来,或者构建新的异常抛出去。这里就涉及到自定义异常,需要确定是否需要自定义checked Exception;在保证诊断信息的同事避免敏感信息,比如java.net.ConnectException的出错信息不包括机器名、IP、端口等敏感信息(日志信息也一样,不可以输出用户信息之类的敏感信息)。
有一种说法,提出Java语言的checkedException是一种设计错误。因为checkedException的初衷是希望捕获异常,再从异常中回复正常,但是大多数情况不能恢复。另外checkedException不兼容functional编程(函数式编程,后续我会展开来讲函数式编程、命令式编程的区别),比如Lambda/Stream代码(后续会详细讲Lambda的语法以及原理)。但也有一部分人提出,确实有一些异常是可恢复的,比如IO异常、网络异常等。
三、异常处理与性能
处理异常会必然会增加代码量,那么接下来我们从性能的角度来审视Java的异常处理机制。
- try-catch代码会影响JVM对代码的优化,所以不要用try包裹大段的代码。
- 不要试图用异常捕获来控制代码流程,比起if/else之类的语句,异常捕获要更加低效。
- Java实例化Exception都会对当时的栈进行快照,该操作较重。对于追求极致性能的底层类库,创建不进行栈快照的Exception是一种方法。但是这样不利于我们定位问题,特别是在微服务这样的分布式系统,会增加诊断的难度。当服务变慢,吞度量下降时,检查最频繁的Exception是一种思路。
四、总结
通过本章的学习,我们了解了Java异常中的两大类:Exception与Error。另外我们也学习了ClassNotFoundException和NoClassDefFoundError之间的区别。Java异常处理机制是Java语言一大特征,遵循异常处理原则可以有效提高代码可读性以及程序运行的稳定性。深入的掌握Java异常处理机制对我们Debug线上问题也有帮助。
五、后记
回顾问题:
- Exception和Error有什么相同点、不通点?
- 常见的CheckedException和RuntimeException有哪些?常见的Error有哪些?
- NoClassDefFoundError和ClassNotFoundException有什么区别?
延伸问题:
- 为什么错误信息打印,不推荐使用e.printStackTrace()?STERR为什么不是个合适的输出项?
- 为什么使用try-catch来控制代码流程,比起if-else更加低效?
- 如何创建不进行栈快照的Exception?