简介
Java语言中Throwable是所有异常的根类,Throwable 派生了两个直接子类Error 和 Exception。Error 表示应用程序本身无法克服和恢复的一种严重问题,触发Error时会终止线程甚至是虚拟机。Exception 表示程序还能够克服和恢复的问题,Exception按照处理时机可以分为编译时异常和运行时异常。
编译时异常都是可以被修复的异常,代码编译期间Java程序必须显式处理编译时异常,否则无法编译通过。运行时异常通常是软件开发人员考虑不周所导致的问题,软件使用者无法克服和恢复这种问题,但在这种问题下软件系统可能会继续运行,严重情况下软件系统才会死掉。
异常的处理
异常处理方案
Java中异常处理有两种方案,捕获处理异常和抛出异常。
对编译时异常处理方案有两种,当前方法知道如何处理该异常则捕获处理。当前方法不知道如何处理则在定义该方法时声明抛出该异常。
运行时异常只有当代码在运行时才发现的异常,编译时不需要捕获处理。如除数是0、数组下标越界等等,其产生频繁,处理麻烦,若显示声明或者捕获将会对程序的可读性和运行效率影响很大。所以由虚拟机自动检测抛出。当然也可以主动显示捕获处理。
异常相关关键字
Java的异常机制主要依赖于try、catch、finally、throw和throws五个关键字。一般try,catch,finally结合使用,用于捕获异常。throws,throw单独使用,用于抛出异常。
// try catch finally
try {
// 可能触发异常的代码
} catch (XXXException e) { // XXXException :代表异常类型
// 这里进行处理异常
} finally {
//这里进行资源释放
}
try后紧跟一个花括号扩起来的代码块简称try块,try块它里面放置可能引发异常的代码。catch后定义一个异常类型和一个代码块。当try块某段代码触发了异常并匹配上catch定义的异常类型,这时便走catch块处理逻辑。
try 代码块后面可以跟着多个 catch 代码块,用于捕获不同类型的异常。Java 虚拟机会从上到下匹配异常处理器。因此,前面的 catch 代码块所捕获的异常类型不能覆盖后面的,否则编译器会报错。
finally块跟catch块之后,finally块用于回收在try块里打开的物理资源,异常机制会保证finally块总被执行。
//throws 抛出异常,方法签名处抛出。
private static void test() throws XXXException {}
//throw 作为语句使用,代码中直接抛出一个异常。 throw new XXXException();
private static void getCode(String type) {
if (type == null) throw new IllegalArgumentException("参数不能为空");
}
throws关键字主要在方法签名中使用,用于声明该方法可能抛出的异常。throw用于抛出一个实际的异常,throw可以单独作为语句使用,抛出一个具体的异常对象。
异常处理栗子
(1)编译时异常
public static void main(String[] args) {
File file = new File("F://a.txt");
if (!file.exists()) {
file.createNewFile(); // 这段代码直接运行,这里编译不通过,报编译时异常。
}
}
编译时异常在代码编译期间就会报错(注意编译期间报错的不一定都是编译时异常),这种异常在编码期间需要手动捕获或者抛出处理~
/**
* Create by SunnyDay on 2022/04/21 18:26
*/
public class ExceptionDemo {
public static void main(String[] args) throws IOException {
tryCatch();
throwsException(); // 这里选择继续抛出给main
}
/**
* 捕获异常栗子
*/
private static void tryCatch() {
try {
File file = new File("F://a.txt");
if (!file.exists()) {
file.createNewFile();
}
} catch (IOException e) {
e.printStackTrace();
} finally {
System.out.println("finally 块");
}
}
/**
* 方法签名处抛出异常栗子。
* 注意若是此方法被其他方法A调用,那么A需要捕获或者抛出处理。
*/
private static void throwsException() throws IOException {
File file = new File("F://a.txt");
if (!file.exists()) {
file.createNewFile();
}
}
}
(2)运行时异常
private static void runtimeException(String name) {
name.length(); //name 为空时java.lang.NullPointerException.直接crash。
}
运行时异常一般为开发人员代码考虑不周引起的,一般不需要主动来捕获或者抛出的~ 不过若是需要也可以主动捕获处理~如下。
/**
* 不过一般不会建议采取捕获处理的方式,完全可通过name的判空处理。
*/
private static void runtimeException(String name) {
// 捕获,出现异常也不会导致crash,不影响try catch 块之外的逻辑。
try {
name.length(); //java.lang.NullPointerException
} catch (Exception e) {
e.printStackTrace();
}
}
(3)访问异常信息
如果需要在catch块中访问异常对象的相关信息,则可以通过访问catch块后的异常形参来获得。当JVM决定调用某个catch块来处理该异常对象时,会将异常对象赋给catch块后的异常参数,这时我们可通过该参数来获得异常的相关信息。常用方法如下:
- getMessage():返回该异常的详细描述字符串。
- printStackTrace():将该异常的跟踪栈信息输出到标准错误输出。
- printStackTrace(PrintStream s):将该异常的跟踪栈信息输出到指定输出流。
- getStackTrace():返回该异常的跟踪栈信息。
注意点
看似两三个栗子吧异常过了一遍,其实异常相关的东西还是很多的~
(1)不管程序代码块是否处于try块中,甚至包括catch块中的代码,只要执行该代码块时出现了异常,系统总会自动生成一个异常对象。如果程序没有为这段代码定义任何的catch块,则Java运行时环境无法找到处理该异常的catch块,程序就在此异常退出。
(2)触发异常时虚拟机会生成对应的异常,并会自上而下遍历catch中定义的异常条目,寻找匹配的异常条目。catch 中定义异常条目时要遵循只能扩大或者不相关的原则,否则编译失败。
(3)在Java 7以前,每个catch块只能捕获一种类型的异常;但从Java 7开始,一个catch块可以捕获多种类型的异常。多异常捕获需要注意:
-
捕获多种类型的异常时,多种异常类型之间用竖线"|"隔开。
-
捕获多种类型的异常时,异常变量有隐式的final修饰,因此程序不能对异常变量重新赋值。
private static void mutipleException(){
// 多异常捕获,如下catch块可以捕获处理2种异常。
try {
}catch (ArrayIndexOutOfBoundsException|NumberFormatException e){
e = new IllegalArgumentException("") // 编译报错,多异常不能重新赋值。
}
// 单个异常捕获
try {
}catch (Exception e){
e = new IllegalArgumentException(""); // 编译通过,单个异常可以赋值。
}
}
(4)在通常情况下,不要在finally块中使用如return或throw等导致方法终止的语句,一旦在finally块中使用了return或throw语句,将会导致try块、catch块中的return、throw语句失效。
(5)try、catch中的return语句、异常触发等导致方法结束的case不会影响finally代码块的执行。
public static void main(String[] args) {
System.out.println("test return value:"+test());
}
private static int test() {
try {
int a = 10 / 0; // ArithmeticException: / by zero
} catch (Exception e) {
return 0;
} finally {
System.out.println("finally");
}
System.out.println("test finish");
return 1;
}
log:
finally
test return value:0
可见finally最终打印出来了,证明了我们的观点,那么为啥方法最终打印的返回值是0,而不是1呢?其实流程是这样的~
首先代码执行到try块触发ArithmeticException异常,然后catch块捕获住处理,不过异常机制有这么一个原则如果在 catch 中遇到了 return 或者异常等能使该函数终止的话,那么finally就必须先执行完finally代码块里面的代码然后再返回到catch中抛出或者return处。最终执行catch return语句方法结束。后续的代码不会再执行了。
不妨可以修改代码验证下,如下catch 代码块执行完后会继续走try catch finally 之外的代码~
private static int test() {
try {
int a = 10 / 0; // ArithmeticException: / by zero
} catch (Exception e) {
System.out.println("catch");
} finally {
System.out.println("finally");
}
System.out.println("test finish");
return 1;
}
log:
catch
finally
test finish
test return value:1
来个栗子再让我们更好巩固下,彻底理解他 emmm~ 如下方法的返回值是几?
private static int test() {
try {
int a = 10 / 0; // ArithmeticException: / by zero
return 1;
} catch (Exception e) {
return 2;
} finally {
return 3;
}
}
代码执行到try的 int a这里会触发ArithmeticException,这时由异常处理器捕获,走catch中return 2,但是由于java的异常执行机制此时会先执行finally中的return3。finally这里正好碰到了return语句,正常结束方法。
若是finally只是处理一些资源关闭的代码,这里未return 3,那么本方法的返回值就是2喽~
(6)除非在try块、catch块中调用了退出虚拟机的方法,否则不管在try块、catch块中执行怎样的代码,出现怎样的情况,异常处理的finally块总会被执行。
private static int test() {
try {
int a = 10 / 0; // ArithmeticException: / by zero
} catch (Exception e) {
System.out.println("catch");
System.exit(0);
} finally {
System.out.println("finally");
}
return 0;
}
log:
catch
Process finished with exit code 0
如上,首先触发ArithmeticException异常,此时会走到catch代码块,执行了打印语句后执行System.exit(0) 直接退出JVM。
(7)try catch finally 执行机制存在异常丢失的情况
/**
* try中捕获异常A,catch中又触发异常B,这时finally执行完后系统只会抛出异常B。
* 这种case也可以看做try catch的弊端,丢失了try中的异常。
* */
private static void test() {
try {
int a = 10 / 0; // ArithmeticException: / by zero
} catch (Exception e) {
String a = null;
a.length(); // finally 执行完毕后这里最终由系统抛出NullPointerException
} finally {
System.out.println("finally");
}
}
/**
* try中捕获异常A,catch中又触发异常B,这时finally执行又触发异常C系统只会抛出异常C。
* 这种case也可以看做try catch的弊端,丢失了try,catch中的异常。
*/
private static void test() {
try {
int a = 10 / 0; // ArithmeticException: / by zero
} catch (Exception e) {
String a = null;
a.length(); // NullPointerException
} finally {
Integer.parseInt("aaa"); //代码执行到这里只会抛出NumberFormatException。上述两异常忽略。
System.out.println("finally");
}
}
Java 7 Supressed 异常以及语法糖
前面了解到try catch 中的异常存在丢失的情况,为了解决这个问题,java7引入了Supressed 异常来解决这个问题。这个新特性允许开发人员将一个异常附于另一个异常之上。因此,抛出的异常可以附带多个异常的信息。
Java 7 专门构造了一个名为 try-with-resources 的语法糖,在字节码层面自动使用 Supressed 异常。当然,该语法糖的主要目的并不是使用 Supressed 异常,而是精简资源打开关闭。因为在 Java 7 之前,对于打开的资源,我们需要定义一个 finally 代码块,来确保该资源在正常或者异常执行状态情况下都能关闭。这种做法使代码太臃肿了~
Java 7 的 try-with-resources 语法糖,极大的简化了try catch finally代码。程序可以在 try 关键字后声明并实例化实现了 AutoCloseable 接口的类,编译器将自动添加对应的 close 操作。在声明多个 AutoCloseable 实例的情况下,编译生成的字节码类似于try catch finally手工编写代码的编译结果。与手工代码相比,try-with-resources 还会使用 Supressed 异常的功能,来避免原异常 “被消失”。
(1)自动关闭资源
系统提供了一些类实现了AutoCloseable接口 ,若直接使用try-with-resources 语法糖则不需要再使用finally做繁琐的关闭处理的工作~
public static void main(String[] args) throws Exception {
/**
* 1、try()中进行变量定义(创建、赋值),类必须实现了AutoCloseable接口(或者是AutoCloseable实现类)。
* 2、try后的代码块中可进行逻辑的操作。
* 3、自动关闭资源的try语句相当于包含了隐式的finally块,执行了close回调,因此这个try语句可以既没有catch块,
* 也没有finally块。
* 4、注意AutoCloseable#close()方法抛出了Exception
* */
try (
BufferedReader br = new BufferedReader(new FileReader("F://a.txt"));
PrintStream pr = new PrintStream(new FileOutputStream("F://b.txt"))
) {
br.readLine();
pr.write("emmm".getBytes());
}
}
BufferedReader、PrintStream都间接实现了AutoCloseable 接口,把它们放在try语句中声明、初始化,try语句会自动关闭它们。当然我们也可以自定义类实现接口即可,在接口中实现资源的处理工作。接下来验证下异常的捕获~
(2)避免异常的丢失
/**
* Create by SunnyDay on 2022/04/22 17:37
*/
public class Demo implements AutoCloseable {
private String desc;
public Demo(String name) {
this.desc = name;
}
public static void main(String[] args) throws Exception {
try (
Demo demo1 = new Demo("1");
Demo demo2 = new Demo("2")) {
int a = 10/0; // 执行代码 触发异常
}
}
@Override
public void close() throws Exception {
// 这里直接抛出一个异常,验证 finally中触发了异常工作。
throw new IllegalArgumentException();
}
}
log: 打印所有的异常信息
Exception in thread "main" java.lang.ArithmeticException: / by zero
at Demo.main(Demo.java:21)
Suppressed: java.lang.IllegalArgumentException
at Demo.close(Demo.java:29)
at Demo.main(Demo.java:22)
Suppressed: java.lang.IllegalArgumentException
at Demo.close(Demo.java:29)
at Demo.main(Demo.java:22)
异常实现原理
异常实例的构造十分昂贵。这是由于在构造异常实例时,Java 虚拟机便需要生成该异常的栈轨迹(stack trace)。该操作会逐一访问当前线程的 Java 栈帧,并且记录下各种调试信息,包括栈帧所指向方法的名字,方法所在的类名、文件名,以及在代码中的第几行触发该异常。
在生成栈轨迹时,Java 虚拟机会忽略掉异常构造器以及填充栈帧的 Java 方法,直接从新建异常位置开始算起。此外,Java 虚拟机还会忽略标记为不可见的 Java 方法栈帧。
既然异常实例的构造十分昂贵,我们是否可以缓存异常实例,在需要用到的时候直接抛出呢?
从语法角度上来看,这是允许的。然而,该异常对应的栈轨迹并非 throw 语句的位置,而是新建异常的位置。因此,这种做法可能会误导开发人员,使其定位到错误的位置。这也是为什么在实践中,我们往往选择抛出新建异常实例的原因。
jvm是如何实现异常的?
class 文件被编译成字节码时,每个方法都附带一张异常表。异常表中的每一个条目代表一个异常处理器。该处理器由from指针、to指针、target指针、所捕获的异常类型组成。这些指针的值是字节码索引,用以定位字节码。
- from 、to 表示表示异常处理器监控范围,即用try代码块监控的范围。
- target表示异常处理器的起始位置,即catch起始位置。
- 异常类型即为xxxException。
/**
* Create by SunnyDay on 2022/04/22 18:45
*/
public class Test {
public static void main(String[] args) {
// 异常条目1(try catch finally块就是一个异常处理器)
try {// from
File file = new File("F://a.txt");
if (!file.exists()) {
file.createNewFile();
}
} catch (IOException e) {//to(不包括to 可以这样记住范围“包左不包右”也即[from,to))。 target,这里也是异常处理器
// 开始位置
e.printStackTrace();
} finally {
System.out.println("finally1");
}
// 异常条目2
try {// from
int a = 1/0;
} catch (Exception e) {// to ,target
e.printStackTrace();
} finally {
System.out.println("finally2");
}
}
}
//javap 命令 查看class文件:javap -c -l Test.class main方法中生成的异常表如下:
Exception table:
from to target type
0 22 33 Class java/io/IOException // 异常条目1
0 22 49 any
33 38 49 any
60 64 75 Class java/lang/Exception // 异常条目2
60 64 91 any
75 80 91 any
当程序触发异常时,Java 虚拟机会生成一个要抛出的异常实例,然后自上至下遍历异常表中的所有条目。当触发异常的字节码的索引值在某个异常表条目的监控范围内,Java 虚拟机会判断要抛出的异常和该条目想要捕获的异常是否匹配。如果匹配,Java 虚拟机会将控制流转移至该条目 target 指针指向的字节码。
如果遍历完所有异常表条目,Java 虚拟机仍未匹配到异常处理器,那么它会弹出当前方法对应的Java 栈帧,并且在调用者中重复上述操作。在最坏情况下,Java 虚拟机需要遍历当前线程 Java栈上所有方法的异常表。最终把异常抛出。
finally 代码块的编译比较复杂,当前版本 Java 编译器的做法,是复制 finally 代码块的内容,分别放在 try-catch 代码块所有正常执行路径以及异常执行路径的出口中。
针对异常执行路径,Java 编译器会生成一个或多个异常表条目,监控整个 try-catch 代码块,并且捕获所有种类的异常。这些异常表条目的 target 指针将指向另一份复制的 finally 代码块。并且,在这个 finally 代码块的最后,Java 编译器会重新抛出所捕获的异常。
如果 catch 代码块捕获了异常,并且触发了另外一个异常,那么 finally 捕获并重抛的异常是哪个呢?答案是后者,也就是说原本的异常便会被忽略掉,这对于代码调试来说十分不利。
UncaughtExceptionHandler
介绍
Java中,当一个线程如果没有显式处理异常而抛出时Jvm会将该异常事件报告给该线程对象的 UncaughtExceptionHandler 进行处理,如果线程没有设置 UncaughtExceptionHandler,则默认会把异常栈信息输出到终端而使程序直接崩溃。所以如果我们想在线程意外崩溃时做一些处理就可以通过实现 UncaughtExceptionHandler 来满足需求。
栗子
/**
* Create by SunnyDay on 2022/04/24 11:07
*/
public class CrashHandler implements Thread.UncaughtExceptionHandler {
private volatile static CrashHandler INSTANCE;
private CrashHandler() {
}
public static CrashHandler getINSTANCE() {
if (INSTANCE == null) {
synchronized (CrashHandler.class) {
if (INSTANCE == null) {
INSTANCE = new CrashHandler();
}
}
}
return INSTANCE;
}
@Override
public void uncaughtException(Thread t, Throwable e) {
printInfo(t, e);
collectDeviceInfo();
saveCatchInfo2File(e);
}
private void printInfo(Thread t, Throwable e) {
System.out.println("异常线程:" + t.getName() + " 异常信息:" + e.getMessage());
}
private void collectDeviceInfo() {
System.out.println("收集用户设备信息");
}
private void saveCatchInfo2File(Throwable ex) {
System.out.println("异常信息保存到文件");
}
}
/**
* Create by SunnyDay on 2022/04/24 11:06
*/
public class Test {
public static void main(String[] args) {
getException();
new Thread(() -> createAnException1(), "工作线程1").start();
new Thread(() -> createAnException2(), "工作线程2").start();
}
/**
* 捕获所有线程未捕获异常。
*/
private static void getException() {
Thread.setDefaultUncaughtExceptionHandler(CrashHandler.getINSTANCE());
}
/**
* 模拟一个异常 "除0异常"
*/
private static void createAnException1() {
int a = 10 / 0;
}
/**
* 模拟一个异常 "NumberFormatException"
*/
private static void createAnException2() {
Integer.parseInt("sss");
}
}
上述Test#main方法运行后最终会走CrashHandler#uncaughtException log如下:
异常线程:工作线程1异常信息:/ by zero
收集用户设备信息
异常信息保存到文件
异常线程:工作线程2异常信息:For input string: "sss"
收集用户设备信息
异常信息保存到文件
源码分析
上面的栗子中使用到了Thread.setDefaultUncaughtExceptionHandler(),其实Thread还有一个方法setUncaughtExceptionHandler那么二者有啥区别呢? 先来个结论再看源码~
- Thread.setDefaultUncaughtExceptionHandler(UncaughtExceptionHandler eh):用于设置一个默认的全局异常处理器,也就是给所有的线程都设置这个处理器。
- Thread.setUncaughtExceptionHandler(UncaughtExceptionHandler eh):给指定线程设置,用于对特定的线程进行未捕获的异常处理。
class Thread implements Runnable {
...
/* The group of this thread */
private ThreadGroup group;
@FunctionalInterface
public interface UncaughtExceptionHandler {
void uncaughtException(Thread t, Throwable e);
}
// 单独线程的UncaughtExceptionHandler 对象
private volatile UncaughtExceptionHandler uncaughtExceptionHandler;
// 全局的UncaughtExceptionHandler 对象
private static volatile UncaughtExceptionHandler defaultUncaughtExceptionHandler;
//设置全局的UncaughtExceptionHandler 对象
public static void setDefaultUncaughtExceptionHandler(UncaughtExceptionHandler eh) {
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
sm.checkPermission(
new RuntimePermission("setDefaultUncaughtExceptionHandler")
);
}
defaultUncaughtExceptionHandler = eh;
}
public static UncaughtExceptionHandler getDefaultUncaughtExceptionHandler(){
return defaultUncaughtExceptionHandler;
}
//为单独线程指定UncaughtExceptionHandler 对象
public void setUncaughtExceptionHandler(UncaughtExceptionHandler eh) {
checkAccess();
uncaughtExceptionHandler = eh;
}
/**
uncaughtExceptionHandler对象为null则返回group。ThreadGroup 类对象在Thread构造中初始化。
这里不妨猜测下ThreadGroup必定实现了UncaughtExceptionHandler接口。
*/
public UncaughtExceptionHandler getUncaughtExceptionHandler() {
return uncaughtExceptionHandler != null ?
uncaughtExceptionHandler : group;
}
...
}
到这就能明白了 setDefaultUncaughtExceptionHandler 和 setUncaughtExceptionHandler 的区别了。线程崩溃时异常抛出的顺序是先调用 Thread 的 getUncaughtExceptionHandler 查看UncaughtExceptionHandler 对象是否有值,如果有就直接处理,没有就调用 ThreadGroup的逻辑处理~
ThreadGroup是UncaughtExceptionHandler 在 JDK 的默认实现类。内部调用 Thread 的 getDefaultUncaughtExceptionHandler() 获取 handler 进行处理,如果默认 handler 也没有处理就直接执行正常的异常流程使程序崩溃~
//这个类实现了UncaughtExceptionHandler 接口。
public class ThreadGroup implements Thread.UncaughtExceptionHandler {
private final ThreadGroup parent;
···
public void uncaughtException(Thread t, Throwable e) {
if (parent != null) {//ThreadGroup 无参构造中直接parent == null。这个值一般为null。
parent.uncaughtException(t, e);
} else {
/**
ThreadGroup 其实也是调用 Thread.getDefaultUncaughtExceptionHandler()来获取UncaughtExceptionHandler 对象的。
当我们通过setDefaultUncaughtExceptionHandler设置过UncaughtExceptionHandler对象时则调用
UncaughtExceptionHandler#uncaughtException
当未设置过UncaughtExceptionHandler对象时打印异常信息,后续就是jvm的crash了~
*/
Thread.UncaughtExceptionHandler ueh =
Thread.getDefaultUncaughtExceptionHandler();
if (ueh != null) {
ueh.uncaughtException(t, e);
} else if (!(e instanceof ThreadDeath)) {
System.err.print("Exception in thread \""
+ t.getName() + "\" ");
e.printStackTrace(System.err);
}
}
}
···
}