Java异常控制机制又被称为“违例控制机制”。
捕获程序错误最理想的时机是在编译阶段,这样可以彻底避免错误的代码运行。
但并非所有的错误都能在编译期间侦测到,有些问题必须在运行期间解决。
错误在运行期间发生时,我们可能不知道具体应该怎样解决,但我们清楚此时不能不管不顾地继续执行下去。此时应该做的事情是:
- 暂停程序的运行:程序不再往下运行
- 指出何时、何地发生了什么样的错误:异常类型,此时的堆栈信息
-
Java异常控制机制的作用流程:
1. 异常产生
首先程序引擎需要能够获知异常的产生。Java中预置了一系列基本的异常条件,如数组下标越界、空指针、被零除等等,这些异常是由JVM自动产生的(也被称为运行时异常,见后);另一部分异常则是由Java代码(可能是JDK的代码或开发人员自己编写的代码)产生的(也被称为checked异常,见后)。
- 异常产生即是异常对象的实例化,该对象的类型通常就说明了异常条件的类型,实例化的异常对象中还会包含对异常条件的补充说明(message),以及异常发生时的线程调用栈信息(stacktrace)。
- 在这个环节中,JAVA完成了对错误的描述,包括错误发生的时间、错误的类型(即异常对象的Class)、对错误的描述(message)和错误发生的位置(stacktrace)。
2. 异常抛出
- 异常抛出是JAVA程序流中的一种特殊流程,当异常产生后,JVM会停止继续执行后面的代码,并将异常对象抛出。抛出的异常对象会进入调用栈的上一层,如果异常对象没有被捕获,它会沿着调用栈的顺序逐层向上抛出,直至调用栈为空,此时该线程的运行也就彻底终止了。
- 异常的抛出解决了当前作用域可能不具备处理异常所需的信息的问题,将异常对象在调用栈中逐级向上传递,直至有能力处理异常的作用域将其捕获。
3. 异常捕获
- 在异常对象逐级向上抛出的过程中,如果调用栈中某一层有捕获该类型异常的逻辑,该异常对象便会被捕捉,异常被捕获后JVM会终止抛出异常对象的过程。
4.异常处理
- 当异常对象被捕获后,JVM会执行捕获后的处理逻辑(处理逻辑是由程序员编写的)。
当处理逻辑执行完成后,JVM会继续执行捕获了异常的作用域中接下来的代码(除非异常处理逻辑中将该异常继续抛出,或异常处理逻辑中产生了新的异常)
Exception 与 Error
前文所述的Java异常控制机制实际上并不仅对“异常”起作用。除了我们所说的异常(Exception)能够被产生、抛出和捕捉之外,还有另一种类型“错误(Error)”。
Java中,
Throwable
是所有可以被抛出并捕获的类的父类。Throwable有两大子类,分别是Exception和Error。- Java官方并没有给出Error和Exception的严格定义,而是将Error描述为“应用程序不应尝试捕捉处理的严重问题”,Exception则是“应用程序应该尝试捕捉处理的问题”。
我们从几个例子看一下:
NoClassDefFoundError
:JVM的ClassLoader在尝试加载某个类,但该类在Classpath中并不存在时会产生的错误。例如a.jar依赖b.jar中的某个类,如果我们使用编译完成的a.jar时并没有引入b.jar,编译器并不会发现问题(因为a.jar已经完成了编译,需要编译的代码中只使用了a.jar中的api,并没有直接使用b.jar),但在运行时JVM找不到b.jar中被a所依赖的类,便会发生错误。UnsupportedClassVersionError
:当JVM尝试加载一个class但发现该class的版本并不被支持时产生的错误。例如我们使用JDK1.8开发并编译一个类,但在JDK1.7的环境中运行时,便会发生此错误OutOfMemoryError
:当JVM内存不足,无法为一个对象分配内存时发生的错误,例如堆区内存溢出、Perm区内存溢出等。StackOverFlowError
:当程序的递归调用过深,导致线程调用栈溢出时发生的错误。NoSuchFieldError/NoSuchMethodError
:当JVM试图访问某个成员属性或某个方法时,发现目标不存在。一般都是由于class信息在运行时被改变导致的,多见于使用反射时。
通过上面的例子能够看出,Error一般都与程序本身的直接关系不大,更多是由于环境导致的问题。而且Error发生后通常程序都没有再继续执行下去的可能性,所以Java官方将其定义为“应用程序不应尝试捕捉处理的严重问题”。
总结:
- Error:需要程序员在程序开始运行前处理完,否则程序无法运行
-
Throw 和 throws 的区别
位置不同
throws
:用在函数上,后面跟的是异常类,可以跟多个throw
:用在函数内,后面跟的是异常对象
功能不同:
- throws 用来声明异常,让调用者只知道该功能可能出现的问题,可以给出预先的处理方式;
throw 抛出具体的问题对象,执行到 throw,功能就已经结束了,跳转到调用者,并将具体的问题对象抛给调用者。也就是说 throw 语句独立存在时,下面不要定义其他语句,因为执行不到。
throws 表示出现异常的一种可能性,并不一定会发生这些异常;
- throw 则是抛出了异常,执行 throw 则一定抛出了某种异常对象。
两者都是消极处理异常的方式,只是抛出或者可能抛出异常,但是不会由函数去处理异常,真正的处理异常由函数的上层调用处理。
Exception的分类
Java将Exception分为两类
- unchecked 异常(运行时异常 RuntimeException )
- RuntimeException 是 Exception 的一个子类,RuntimeException 的子类都属于 unchecked 异常(也就是运行时异常)
- Java要求checked异常必须被在代码编写阶段就调用者了解
- checked 异常(非运行时异常 )
- 除了 RuntimeException,其他所有的Exception都是checked异常(也就是非运行时异常)
- unchecked异常不用在代码编写阶段就被调用者了解
这两种异常的区别从字面上即可理解,checked代表“必须被check”,而unchecked代表“无须被check”:
如果一个方法中有可能产生checked异常,则Java编译器会要求该方法定义中必须加入throws定义,明确说明该方法可能会抛出某类checked异常。
如下图:
foo方法可能产生 IOException(这是一种checked异常),所以bar方法在调用foo时,编译器会提示错误。此时可以在bar方法的定义行中加入throws:
public void bar() throws IOException
也可以在bar方法内将IOException捕获处理:
另一个理解checked异常与unchecked异常区别的角度是
- 所有由JVM自动生成的异常都是unchecked异常
- 由java程序主动生成的异常是checked异常。
例如:
上图中 f.createNewFile()
方法可能会产生checked异常 IOException
,我们看看File类的源码:
可以看到红框处,IOException异常是在代码中被主动抛出的,凡是这样在代码中主动抛出的异常,都是checked异常。
相应地,unchecked异常是JVM在运行时自动产生的,例如下图的方法,只要传入的参数b等于0,就会在运行时 jvm 自动产生 ArithmeticException
代码中永远不需要这样写:
异常处理的原则
异常处理的原则主要有三个
- 具体明确
- 提早抛出
-
具体明确
指抛出的异常应能通过异常类名和message准确说明异常的类型和产生异常的原因。
我们通过例子来看:
代码1 将多个参数的异常情况混合到一个异常提醒中
异常类型为父类 Exception
代码2每个参数的校验抛出各个参数的具体异常信息
异常类型为具体的参数非法异常
这两段代码的处理逻辑是类似的,均是在入参 input1 或 input2 为 null 或空串时抛出异常,但只有第二段符合“具体明确”的标准:
- 第二段代码通过异常类型【IllegalArgumentException】明确了异常是由于传入了不合法的参数导致的;
- 在message中说明了具体是哪个参数不合法,为什么不合法。这样不仅能够在查阅日志时快速知晓异常产生的原因,也让上层的程序能够针对
IllegalArgumentException
这一特定类型的异常进行有针对性的捕捉和处理。
相比之下,第一段代码中抛出的异常就不够具体明确,异常类型Exception不具有说明性质,异常message也不够明确,上层程序难以处理,阅读日志时也难以快速定位。
提早抛出
指应尽可能早的发现并抛出异常,便于精确定位问题。
同样通过例子来看:
代码1
代码2
在传入的filename为null时,这两段代码都会抛出异常,第一段代码抛出的异常是
- 抛出的异常是在标准Java类库【InputFileStream】中抛出的,提升了问题定位的难度。幸好stacktrace中也打印出了前面的调用链,我们可以在标准类库的调用者身上查找问题(可以定位到Test.java的第38行)
- NullPointerException是Java中信息量最少的(却也是最常遭遇且让人崩溃的)异常。它压根不提我们最关心的事情:到底哪里是null
第二段代码抛出的异常是
- 对filename提前进行了校验,并以
IllegalArgumentException
的形式抛出 - 在第一段代码中遇到的两个问题都可以得到解决,这便是提早抛出(预判抛出更具体的异常)的好处
延迟捕获
指异常的捕获和处理应尽可能延迟,让掌握更多信息的作用域来处理异常。
代码1
- readSomeFile 方法将 new FileInputStream 处有可能产生的 FileNotFoundException 捕获,并将异常信息记录到了日志中,也即:在底层通用方法处理掉了 FileNotFoundException 异常
- 但 readSomeFile 这个方法有可能是一个通用的底层方法,会在各种业务场景下被调用,不同的业务场景下,发生FileNotFoundException 时的处理策略可能不一样
代码2
- 没有对FileNotFoundException加以处理,而是直接在方法定义中将其抛出
- 将异常抛出交由掌握了足够多信息的上层调用者捕获,这样就可以根据异常产生所处的具体业务流程来进行不同的处理
例如我们可以在一个业务逻辑中这样处理
- 文件不存在时,尝试读取备份文件
同时在另一个业务逻辑中这样处理
- 文件不存在时,发送异常信息
其他重要原则
不要让异常逃掉
当一个异常在整个调用栈中的任意一层都没有被捕获,这个异常就“逃掉”了。这对于任何程序来说都是一个灾难性的事件。
- 对于B/S系统,从请求处理线程中逃掉的异常很可能会被B/S框架(如Struts/SpringMVC等)捕捉到。如果没有正确配置,这些逃掉的异常很可能就被框架“吃掉”了,即框架捕获了从业务代码层抛出的异常,且没有记录或没有完整记录异常信息。这样的异常来无影去无踪,完全无迹可寻,堪称程序员的大敌。
- 某些情况下,异常会被抛到中间件或容器(Tomcat/Jboss/Weblogic/Websphere等)层(可能是没有使用B/S框架或B/S框架没有“吃掉”异常)。被中间件或容器捕获到的异常,一般情况下会被记录在中间件或容器自己的日志中(也有可能不会记),但问题在于,这种情况下,用户会看到中间件或容器提供的错误页,这些错误页基本没有用户友好型可言,而且有可能会把异常堆栈的信息直接显示在页面上,在开放性的系统中,暴露堆栈信息极有可能引发严重的安全问题。
- 而在后台进程中,如果异常逃掉了,将会导致线程的退出。如果没有守护线程及时补充异常退出的线程,那么将有可能发生整个进程因为异常而中止的灾难性后果。
所以说,在编程时应绝对避免异常“逃逸”的情况,对于B/S系统来说,我们可以在每个Action中都加入try-catch块,捕获所有Exception,也可以利用B/S框架的特性来实现从Action层抛出的异常的统一处理(如Struts2和SpringMVC都有的拦截器机制)。对于后台进程来说,可以利用try-catch块避免异常导致线程中止,也可以通过添加守护线程来及时补充因异常而退出的线程,同时还应使用Thread.setDefaultUncaughtExceptionHandler来确保未捕获异常的正确记录。
正确记录异常信息
即在异常的 stack trace 信息完整、未缺失的基础上,确保异常的 stack trace 被正确记录到日志中
错误的做法:
上面的5种处理全都是错误的前两种将异常信息输出到了控制台而不是日志文件中。
- 后三种错误的使用了 log4j 的 error 方法,均没有正确记录异常的 stacktrace
正确的方法:
注意应使用正确的error方法,传入两个参数,参数1是对异常的附加描述,参数2是未被篡改过的异常对象
在某些情况下,可能需要在处理异常后继续抛出,让上层捕获后继续处理,在这种情况下,需要注意抛出的异常对象未被篡改。
错误的
- 下层的异常 stack trace 会全部被吃掉
正确的写法
- 直接抛出未被篡改的异常对象
Try—catch—
try 语句允许我们定义在执行时进行错误测试的代码块。
catch 语句允许我们定义当 try 代码块发生错误时,所执行的代码块。
finally 语句在 try 和 catch 之后无论有无异常都会执行。
1.try中带return的情况:try{return}catch(){}finally{}
@Test
public void test01(){
System.out.println("结果--"+tryTest());
}
public int tryTest(){
int a=1;
try {
a=11;
System.out.println("try:"+a);
return a+1;
}catch (Exception e){
a=22;
}finally {
a=33;
System.out.println("finally: "+a);
}
return a;
}
try中 return 时返回值被保存,等 finally 执行完之后才能 return 完成;
2.catch中带 return 的情况:try{}catch(){return}finally{} 与try中带return一样
catch中return时返回值被保存,等 finally 执行完之后才能 return 完成;
3. finally中带return的情况:try{return}catch(){return}finally{return}
由于finally块中有return,会使程序提前退出并不执行 try 或 catch 中的 return。
注:
- 如果finally存在的话,任何执行try 或者catch中的return语句之前,都会先执行finally语句。
- 如果finally中有return语句,那么程序就return了,所以finally中的return是一定会被return的,编译器把finally中的return实现为一个warning。
try-catch-finally
前文所述的异常控制流程,在JAVA程序中以try-catch-finally结构实现:
- try块也被称为“警戒区”,try块包裹的代码在执行过程如果产生异常,或其调用栈的下层中产生了异常并被抛至本层,则会被与此try块关联的catch命令尝试捕获。若异常产生于警戒区之外,则会直接向上层抛出。
- catch命令后的括号内指定希望捕捉的异常对象类型(可以指定多个),如果产生或被抛至此层的异常对象是catch指定的异常类型(或其子类),则异常对象会被捕捉。上例中,所有Exception对象及其子类的对象在此处均会被捕获。
- 被捕获后,JVM会执行catch块中的代码,catch块中的代码能够访问被捕捉到的异常对象(即上例中的Exception e)。
- catch块中的代码仍然有可能产生异常,所以也可以在catch块中插入try-catch-finally。
finally块为可选块,如果有,则无论是否有异常被抛出,JVM都会在try-catch块执行完成后执行finally块中的代码。
实战日志打印error:
public static void main(String[] args) {
for (int i = 0; i < 18; i++) {
try {
addNum(i);
System.out.println(i);
}catch (Exception e){
log.error("处理生日活动发生异常",e);
}
}
}
public static int addNum(int a){
int total=100+a;
if (a==10){
return 1/0;
}
return total;
}
日志打印error:
public static void main(String[] args) {
for (int i = 0; i < 18; i++) {
try {
addNum(i);
System.out.println(i);
}catch (Exception e){
log.error("Service Exception...参数{}", i, e);
// log.error("Service Exception...参数{},{}", i,"aaa", e);
}
}
}
public static int addNum(int a){
int total=100+a;
if (a==10){
return 1/0;
}
return total;
}
```java log.error(“生日活动组装条件发生异常{},{}”, activityBirthday.getActivityName(),activityBirthday.getActivityCode(), e);
// log.error(“Service Exception…参数{},{}”, i,”aaa”, e);
![](https://img-blog.csdnimg.cn/2019110309141933.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MTk4ODg3NQ==,size_16,color_FFFFFF,t_70#id=nAQ5C&originHeight=361&originWidth=777&originalType=binary&ratio=1&status=done&style=shadow)
```java
public static void main(String[] args) {
for (int i = 0; i < 18; i++) {
addNum(i);
System.out.println(i);
}
}
public static int addNum(int a) {
int total = 0;
try {
total = 100 + a;
if (a == 10) {
return 1 / 0;
}
return total;
} catch (Exception e) {
log.error("Service Exception...参数{}", a, e);
}
return total;
}
循环中try—catch--
ublic static void main(String[] args) {
for (int i = 0; i <10 ; i++) {
Student po = null;
try {
po = getPo();
System.out.println(po);
} catch (Exception e) {
log.error("Service Exception...参数{},{}", po,"aaa", e);
}
System.out.println(i+"----"+po);
}
}
public static Student getPo() {
Student student = new Student();
student.setAaa("1111");
student.setName("22222");
int i = 1 / 0;
student.setAddr("999999");
return student;
}
在catch中返回结果
@Slf4j
public class TestDemo {
@Test
public void test01(){
System.out.println("结果--"+tryTest());
}
public int tryTest(){
try {
int i = 1 / 0;
return 0;
}catch (Exception e){
log.error("异常",e);
return 1;
}
}
}
@Test
public void test01(){
System.out.println("结果--"+tryTest());
}
public int tryTest(){
int a=0;
try {
int i = 1 / 0;
a=1; //异常后改动
return a;
}catch (Exception e){
log.error("异常",e);
return a;
}
}
@Test
public void test01(){
System.out.println("结果--"+tryTest());
}
public int tryTest(){
int a=0;
try {
a=1; //异常前改动
int i = 1 / 0;
return a;
}catch (Exception e){
log.error("异常",e);
return a;
}
}