概述
在Java中任务或方法的运行,要不就是正常执行完成(包括虚拟机退出,比如System.exit()
),要不就是出现异常终止(Throwable
)。本章节重点讲解在Java中对异常的处理。当程序出现异常之后,Java会抛出一个封装好的异常堆栈信息,并且终止当前的方法,异常处理机制会将代码执行交给异常处理器。整体结构如下图所示:

异常分类
Throwable
1 | * The {class is the superclass of all errors and Throwable} |
在Java中Throwable
是所有异常类的父类,只有该类的子类才能被用于Java异常处理。该类的唯一两个子类是Error
和Exception
。
Error
1 | * An { Error} is a subclass of { Throwable} |
Error在正常情况下不应该出现的异常(一般是JVM本身产生的异常,比如JVM运行错误、NoClassDefFoundError
或OutOfMemoryError
),而且不建议应用程序对其进行捕获。Error被定义为非检查异常。Error 类是指 java 运行时系统的内部错误和资源耗尽错误。应用程序不会抛出该类对象。如果出现了这样的错误,除了告知用户,剩下的就是尽力使程序安全的终止。 包括以下类:

Exception(RuntimeException、CheckedException)
1 | * <p>The class { Exception} and any subclasses that are not also |
Exception
用于处理应用程序方面的异常定义和处理。分为RuntimeException
和CheckedException
(非RuntimeException
)。检查类异常需要在方法或者构造器中明确的进行处理(throws)。
RuntimeException
1 | * { RuntimeException} is the superclass of those |
RuntimeException
是在JVM的正常操作期间可以抛出异常的超类。该类都是未检查异常类,未检查异常不需要在方法或构造函数中进行throws,如果他们可以通过该方法或构造函数的执行被抛出和方法或构造边界之外传播。所以通常用不着捕获RuntimeException,但在自己的封装里,也许仍然要选择抛出一部分RuntimeException。常见的类包括:

CheckedException
Exception下除RuntimeException
和Error
之外的异常类都是CheckedException
。它们都在java.lang库内部定义。Java编译器要求程序必须捕获或声明抛出这种异常。常用的类包括: I/O 错误导致的 IOException、SQLException。这类异常的表现形式一般为:
- 试图在文件尾部读取数据
- 试图打开一个错误格式的 URL
- 试图根据给定的字符串查找 class 对象,而这个字符串表示的类并不存在
CheckedException与RuntimeException的区别
- CheckedException需要显示的处理,throws;RuntimeException不需要。
- RuntimeException运行期间的错误,一般都是代码bug;CheckedException编译期间的错误,一般是外部错误。Java 编译器会强制程序去捕获此类异常( try catch)。
异常的处理方式
抛出
当程序中出现异常时,如果不进行具体处理,可以使用throw
、throws
、系统自动抛出三种方式进行异常抛出处理。
throw与throws的区别
- 位置不同:throws在函数或构造器定义中,throw是函数或构造器内
- 功能不同:throws用于异常声明,让调用者知道可能出现的异常;throw抛出异常,业务处理终止,抛出到上层业务
- 含义不同:throws 表示出现异常的一种可能性,并不一定会发生这些异常;throw 则是抛出了异常,执行 throw 则一定抛出了某种异常对象。 throw需要配合throws使用。
- 两者都是消极处理异常的方式,只是抛出或者可能抛出异常,但是不会由函数去处理异常,真正的处理异常由函数的上层调用处理。
捕获
如果程序中需要进行异常的特殊处理或者进行异常转换,则需要使用try、catch进行处理。
处理的伪代码
1 | try{ |
首先执行try中包含的代码块,如果遇到执行错误,程序掷出(throw)一特定类型的违例,你捕捉到此违例并转而执行catch中的违例控制代码。最后,无论程序是否产生违例都必须执行finally中的代码,其主要为一些变量清除、资源回收(1)等工作。
异常的限制
- 重写一个方法时,只能产生已在方法的基础类版本中定义的异常。
- 重写的方法可以抛出父类方法所抛出的异常或它的子类型
- 重写的方法可以不用抛出父类方法所抛出的异常
- 重写的方法不可以抛出异常如果父类方法没有抛出异常
- 对异常的限制并不适用于构建器。
异常匹配
掷”出一个异常后,异常控制系统会按当初编写的顺序搜索“最接近”的控制器。一旦找到相符的控制器,就认为异常已得到控制,不再进行更多的搜索工作。在异常和它的控制器之间,并不需要非常精确的匹配。一个衍生类对象可与基础类的一个异常控制器相配,即我们在写代码时,将子类写在前面。
JVM中处理异常的原理
异常的执行顺序
1、new一个异常对象
2、终止当前的执行程序。
3、弹出异常对象的引用。
4、异常处理机制接管被终止的执行程序。
5、寻找一个恰当的地点(异常处理程序)继续执行程序。
异常处理的理论模型
- 终止模型:这种模型将假设错误非常关键,以至于程序无法返回到异常发生的地方继续执行,一旦异常抛出错误就意味着世界末日,意味着死亡,意味着GG
- 恢复模型:异常处理程序发现了错误,并且修复了错误然后重新调用出问题的方法,并且认为第二次调用该方法会成功。通常可以将try块放入while循环中,不断执行方法,直到得到满意的结果。
源码分析
异常信息构建:Throwable
1 | /** |
核心代码:fillInStackTrace
1 |
|
StackTraceElement
1 | /** |
其他
违例的作用
1)监视程序中的异常情况
2)当异常情况发生时,将控制权交给你自己编写的违例控制代码
使用准则
(1) 解决问题并再次调用造成违例的方法。
(2) 平息事态的发展,并在不重新尝试方法的前提下继续。
(3) 计算另一些结果,而不是希望方法产生的结果。
(4) 在当前环境中尽可能解决问题,以及将相同的违例重新“掷”出一个更高级的环境。
(5) 在当前环境中尽可能解决问题,以及将不同的违例重新“掷”出一个更高级的环境。
(6) 中止程序执行。
(7) 简化编码。若违例方案使事情变得更加复杂,那就会令人非常烦恼,不如不用。
(8) 使自己的库和程序变得更加安全。这既是一种“短期投资”(便于调试),也是一种“长期投资”(改善应用程序的健壮性)
异常的处理包括业务类处理(给于用户更好的友好提示)和bug类处理(链条式异常信息输出,方便运维人员或研发人员快速定位问题)。
异常相关的关键字
try,catch,throw,throws,finally
finally的使用总结
finally不被执行的场景
- 与try配套使用,所以只有try执行finally才会执行
- 如果try中执行System.exit(0);或jvm异常终止,则否finally不会被执行
finally语句在return语句执行之后return返回之前执行
1 | package com.sunld.finally1; |
1 | package com.sunld.finally1; |
finally块中的return语句会覆盖try块中的return返回
1 | package com.sunld.finally1; |
这说明finally里的return直接返回了,就不管try中是否还有返回语句.
finally语句中没有return语句覆盖返回值,返回值的变化
用例1:
1 | package com.sunld.finally1; |
用例2:
1 | package com.sunld.finally1; |
try块里的return语句在异常的情况下不会被执行
1 | package com.sunld.finally1; |
当发生异常后,catch中的return执行情况与未发生异常时try中return的执行情况完全一样
1 | package com.sunld.finally1; |
总结
- finally语句在return语句执行之后return返回之前执行
- finally块中的return语句会覆盖try块中的return返回
- 如果finally语句中没有return语句,且覆盖了返回值,那么原来的返回值原始类型则不覆盖,对象类型则覆盖
- try块里的return语句在异常的情况下不会被执行
- 当发生异常后,catch中的return执行情况与未发生异常时try中return的执行情况完全一样。
自定义异常
自定义异常的优点
- 统一了对外异常展示的方式。
- 方便框架统一处理
@ControllerAdvice
- 定义业务类异常
- 隐藏底层的异常,这样更安全,异常信息也更加的直观
自定义异常的注意事项
- 所有异常都必须是 Throwable 的子类。
- 如果希望写一个检查性异常类,则需要继承 Exception 类。
- 如果你想写一个运行时异常类,那么需要继承 RuntimeException 类。
异常捕获的陷阱
正确关闭资源的方式
对于物理资源(数据库连接、网络连接、磁盘文件),JVM是不会进行处理的,因为JVM属于Java内存管理的一部分,只负责回收堆内存中分配的空间。
关闭资源:
- 必须要保证一定执行,一次要放在finally中完成
- 必须保证被关闭的资源不为空
- 保证资源之间的关闭操作互不影响
finally块的陷阱
finally块的执行规则
如果调用了System.exit(0);finally将不再执行,
当System.exit(0)被执行时,虚拟机在退出之前要完成两项工作:
- 执行系统中注册的所有钩子
- 如果程序调用了System.runFinalizersOnExit(true);那么JVM会对所有未结束的对象调用Finalize
1
2
3
4
5
6
7
8
9
10
11
12
13final FileOutputStream fos = new FileOutputStream("");
Runtime.getRuntime().addShutdownHook(new Thread(){
public void run(){
if(fos!=null){
try {
fos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
});
System.exit(0);finally块和方法返回值
当Java程序执行try、catch遇到return语句时,return语句会导致该方法会立即结束;系统执行return语句之后并不会立即结束该方法,而是去寻找异常处理过程中是否有finally,如果有则会执行finally代码块,在执行finally块时如果该块中没有return则会直接返回到try中的return,结束该方法,如果有则会直接返回finally中的数据,而不会调用try中的return。
catch的用法
- catch的顺序: 先处理小异常在处理大异常
- 不要用catch代替流程控制
- 只能catch可能抛出的异常(减少大范围catch异常)
- 实际的修复
- 如果程序知道如何修复这个异常,应该在catch中修复这个异常,修复之后可以再次调用这个方法;
- 如果程序不知道如何修复并且系统也没有进行任何修复,千万不要再次调用可能导致该异常的方法。(造成内存溢出),不要在finally块中调用可能引起异常的方法,可能会导致无限递归、内存溢出
继承得到的异常
- 子类重写父类方法时,不能抛出比父类方法类型更多、范围更大的异常
- 抛出的异常只能是父类异常中的交集,否则不能通过编译。
异常的处理流程

异常拦截
系统的异常处理机制是衡量一个系统设计的关键因素,良好的异常处理机制能在系统出现异常时准确的找到问题的所在。spring aop对异常的处理有良好的支持。spring(spring全家桶中增加了很多异常统一处理的接口和AOP,比如@ControllerAdvice
) 提供了一个接口 ThrowsAdvice
,该接口里面没有任何方法,但是实现类里面必须的实现。
1 | //可以处理详细的异常信息 |
ClassNotFoundException和NoClassDefFoundError的区别
NoClassDefFoundError是一个错误(Error),而ClassNOtFoundException是一个异常,在Java中错误和异常是有区别的,我们可以从异常中恢复程序但却不应该尝试从错误中恢复程序。
ClassNotFoundException的产生原因
- 使用
Class.forName(ClassLoader.loadClass、ClassLOader.findSystemClass)
加载对象时,如果没有找到则会出现该异常 - 当一个类已经某个类加载器加载到内存中了,此时另一个类加载器又尝试着动态地从同一个包中加载这个类。
- ClassNotFoundException发生在装入阶段。
- 加载时从外存储器找不到需要的class就出现ClassNotFoundException
NoClassDefFoundError产生的原因
- JVM或者ClassLoader实例尝试加载(可以通过正常的方法调用,也可能是使用new来创建新的对象)类的时候却找不到类的定义。要查找的类在编译的时候是存在的,运行的时候却找不到了。
- NoClassDefFoundError: 当目前执行的类已经编译,但是找不到它的定义时
- 连接时从内存找不到需要的class就出现NoClassDefFoundError
NoClassDefFoundError 解决的三种方法
Simple example of NoClassDefFoundError is class belongs to a jar and jar was not added into classpath or sometime jar’s name has been changed by someone like in my case one of my colleague has changed tibco.jar into tibco_v3.jar and by program is failing with java.lang.NoClassDefFoundError and I was wondering what’s wrong.
首先是类在运行的时候依赖于其它的一个jar包,但是该jar包没有加载到classpath中或者是该jar包的名字被其他人改了,就像我的一个例子tibo.jar改为了tibco_v3.jar…….Class is not in Classpath, there is no sure shot way of knowing it but many a times you can just have a look to print System.getproperty(”java.classpath“)and it will print the classpath from there you can at least get an idea of your actual runtime classpath.
运行的类不在classpath中,这个问题没有一个确定的方法去知道,但是很多时候你可以通过System.getproperty(”java.classpath“)方法,该方法能让你至少可以领略到实际存在的运行期间的classpath。Just try to run with explicitly -classpath option with the classpath you think will work and if its working then it’s sure short sign that some one is overriding java classpath.
试着通过-classpath命令明确指出你认为正确的classpath,如果能够正常执行的话就说明你使用的classpath是正确的,而系统中的classpath已经被修该过了。
类装载方式
显示类装载
显式 类装入发生在使用以下方法调用装入的类的时候:
- cl.loadClass()(cl 是 java.lang.ClassLoader 的实例)
- Class.forName()(启动的类装入器是当前类定义的类装入器)
当调用其中一个方法的时候,指定的类(以类名为参数)由类装入器装入。如果类已经装入,那么只是返回一个引用;否则,装入器会通过委托模型装入类。
隐式类装载
隐式 类装入发生在由于引用、实例化或继承导致装入类的时候(不是通过显式方法调用)。在每种情况下,装入都是在幕后启动的,JVM 会解析必要的引用并装入类。与显式类装入一样,如果类已经装入了,那么只是返回一个引用;否则,装入器会通过委托模型装入类。