ThinkChat🤖让你学习和工作更高效,注册即送10W Token,即刻开启你的AI之旅 广告
[TOC] # 简介 Java 标准库内建了一些通用的异常,这些类以 Throwable 为顶层父类,Throwable 又派生出`Error`类和`Exception`类: * **Error(错误):**Error 类以及他的子类的实例,一般标识 JVM 本身的错误。错误不能被程序员通过代码处理,Error 很少出现。因此,程序员应该关注Exception为父类的分支下的各种异常类。 * **Exception(异常):**Exception 以及他的子类,代表程序运行时发送的各种不期望发生的事件。可以被 Java 异常处理机制使用,是异常处理的核心。 根据 JAVAC 对项目在编译时是否进行检查又可以分为 unckecked exception(非检查异常)与 checked exception(检查异常): * **Unckecked Exception(非检查异常):**Error 和 RuntimeException 以及他们的子类在编译时,发现与提醒存在异常信息,所以也就不会发现与处理这些异常。对于这些异常发生的原因多半是代码写的有问题导致,一般我们应该修正代码,而不是去通过异常处理器处理。 * **Checked Exception(检查异常):**除了 Error 和 RuntimeException 之外的其它异常,JAVAC 强制要求开发人员提前预备处理异常工作,如使用`try-catch`,`finally`或者`throws`,在方法中要么用 try-catch 捕获并处理异常,要么用 throws 声明抛出交给上级处理,否则编译将不会通过。这样的异常一般是由开发人员的运行环境引起的,因为程序可能被运行在各种未知的环境下,而开发人员无法干预用户如何使用他编写的程序,于是开发人员就应该提前考虑到可能发生的异常信息并处理。 **非检查异常如:** * ArithmeticException * ClassCastException * ArrayIndexOutOfBoundsException * NullPointerException **检查异常如:** * SQLException * IOException * ClassNotFoundException # 异常类结构图 ![](https://img.kancloud.cn/16/d3/16d3e4b366b0c6cb5a6a0fc9a67c717e_636x395.png) # 异常关键字 * **try:**try 代码块中放可能发生异常的代码,当发生异常时候会调用后面的 catch 处理异常。 * **catch:**当 try 代码块中的代码出现异常时,按代码顺序依次和多个 catch 中设置的异常类型进行检测,如果类型匹配就进行异常处理。 * **throw:**用来在方法中使用其抛出指定的异常对象,当抛出异常时会检查该代码中是否有处理异常逻辑,有处理即按照处理逻辑运行,未处理时程序将抛出异常信息,然后程序终止。 * **throws:**用在方法头部中声明抛出多个异常,调用方法会提醒处理异常,不处理即不能通过编译。 * **finally:**不管异常是否出现都会执行 finally 代码块中的内容,这个代码块中主要做一些清理资源的工作,如流的关闭,数据库连接的关闭等,不推荐用于 return 返回信息。 # 异常的继承体系 在Java中使用Exception类来描述异常 ![](https://box.kancloud.cn/d91f6bb6148147defaaaa79e00288cc6_930x100.png) 我们可以发现Exception有继承关系,它的父类是Throwable。Throwable是Java 语言中所有错误或异常的超类 ![](https://box.kancloud.cn/bc76656c5087f0ba6328005e1185d0f1_916x361.png) 另外,在异常Exception类中,有一个子类要特殊说明一下,RuntimeException子类,RuntimeException及其它的子类只能在Java程序运行过程中出现 异常继承体系总结: ~~~ Throwable: 它是所有错误与异常的超类(祖宗类) |- Error 错误,不能处理只能避免 |- Exception 编译期异常,进行编译JAVA程序时出现的问题 |- RuntimeException 运行期异常, JAVA程序运行过程中出现的问题 ~~~ # 抛出异常throw 在java中,提供了一个throw关键字,它用来抛出一个指定的异常对象。那么,抛出一个异常具体如何操作呢? 1. 创建一个异常对象。封装一些提示信息(信息可以自己编写)。 2. 需要将这个异常对象告知给调用者。怎么告知呢?怎么将这个异常对象传递到调用者处呢?通过关键字throw就可以完成。throw 异常对象; throw用在方法内,用来抛出一个异常对象,将这个异常对象传递到调用者处,并结束当前方法的执行。 使用格式: ~~~ throw new 异常类名(参数); ~~~ 例如: ~~~ throw new NullPointerException("要访问的arr数组不存在"); throw new ArrayIndexOutOfBoundsException("该索引在数组中不存在,已超出范围"); ~~~ 下面是异常类ArrayIndexOutOfBoundsException与NullPointerException的构造方法 ![](https://box.kancloud.cn/c75ee634dfb27bf41ce338f4d6ce76be_937x349.png) # 声明异常throws 声明:将问题标识出来,报告给调用者。如果方法内通过throw抛出了编译时异常,而没有捕获处理(稍后讲解该方式),那么必须通过throws进行声明,让调用者去处理。 声明异常格式: ~~~ 修饰符 返回值类型 方法名(参数) throws 异常类名1,异常类名2… { } ~~~ ## throw与throws的比较 1. throws出现在方法函数头;而throw出现在函数体。 2. throws表示出现异常的一种可能性,并不一定会发生这些异常;throw则是抛出了异常,执行throw则一定抛出了某种异常对象。 # 捕获异常`try…catch…finally` 捕获:Java中对异常有针对性的语句进行捕获,可以对出现的异常进行指定方式的处理 捕获异常格式: ~~~ try { //需要被检测的语句。 } catch(异常类 变量) { //参数。 //异常的处理语句。 } finally { //一定会被执行的语句。 } ~~~ try:该代码块中编写可能产生异常的代码。 catch:用来进行某种异常的捕获,实现对捕获到的异常进行处理。 finally:有一些特定的代码无论异常是否发生,都需要执行。另外,因为异常会引发程序跳转,导致有些语句执行不到。而finally就是解决这个问题的,在finally代码块中存放的代码都是一定会被执行的。 # 运行时期异常 * RuntimeException和他的所有子类异常,都属于运行时期异常。NullPointerException,ArrayIndexOutOfBoundsException等都属于运行时期异常. * 运行时期异常的特点: * 方法中抛出运行时期异常,方法定义中无需throws声明,调用者也无需处理此异常 * 运行时期异常一旦发生,需要程序人员修改源代码. # 异常在方法重写中细节 * **子类覆盖父类方法时,如果父类的方法声明异常,子类只能声明父类异常或者该异常的子类,或者不声明。** 例如: ~~~ class Fu { public void method () throws RuntimeException { } } class Zi extends Fu { public void method() throws RuntimeException { } //抛出父类一样的异常 //public void method() throws NullPointerException{ } //抛出父类子异常 } ~~~ * **当父类方法声明多个异常时,子类覆盖时只能声明多个异常的子集。** 例如: ~~~ class Fu { public void method () throws NullPointerException, ClassCastException{ } } class Zi extends Fu { public void method()throws NullPointerException, ClassCastException { } public void method() throws NullPointerException{ } //抛出父类异常中的一部分 public void method() throws ClassCastException { } //抛出父类异常中的一部分 } ~~~ * **当被覆盖的方法没有异常声明时,子类覆盖时无法声明异常的**。 例如: ~~~ class Fu { public void method (){ } } class Zi extends Fu { public void method() throws Exception { }//错误的方式 } ~~~ 举例:父类中会存在下列这种情况,接口也有这种情况 问题:接口中没有声明异常,而实现的子类覆盖方法时发生了异常,怎么办? 答:无法进行throws声明,只能catch的捕获。万一问题处理不了呢?catch中继续throw抛出,但是只能将异常转换成RuntimeException子类抛出 ~~~ interface Inter { public abstract void method(); } class Zi implements Inter { public void method(){ //无法声明 throws Exception int[] arr = null; if (arr == null) { //只能捕获处理 try{ throw new Exception(“哥们,你定义的数组arr是空的!”); } catch(Exception e){ System.out.println(“父方法中没有异常抛出,子类中不能抛出Exception异常”); //我们把异常对象e,采用RuntimeException异常方式抛出 throw new RuntimeException(e); } } } } ~~~ # 异常中常用方法 在Throwable类中为我们提供了很多操作异常对象的方法,常用的如下 ![](https://box.kancloud.cn/351530123f49a8710dd36698718603e5_919x279.png) * getMessage方法:返回该异常的详细信息字符串,即异常提示信息 * toString方法:返回该异常的名称与详细信息字符串 * printStackTrace:在控制台输出该异常的名称与详细信息字符串、异常出现的代码位置 ~~~ try { Person p= null; if (p==null) { throw new NullPointerException(“出现空指针异常了,请检查对象是否为null”); } } catch (NullPointerException e) { String message = e.getMesage(); System.out.println(message ); String result = e.toString(); System.out.println(result); e.printStackTrace(); } ~~~ # 自定义异常 **java中所有的异常类,都是继承Throwable,或者继承Throwable的子类。这样该异常才可以被throw抛出。** **说明这个异常体系具备一个特有的特性:可抛性:即可以被throw关键字操作。** 并且查阅异常子类源码,发现每个异常中都调用了父类的构造方法,把异常描述信息传递给了父类,让父类帮我们进行异常信息的封装。 例如NullPointerException异常类源代码: ~~~ public class NullPointerException extends RuntimeException { public NullPointerException() { super();//调用父类构造方法 } public NullPointerException(String s) { super(s);//调用父类具有异常信息的构造方法 } } ~~~ * 自定义异常继承Exception演示 ~~~ class MyException extends Exception{ /* 为什么要定义构造函数,因为看到Java中的异常描述类中有提供对异常对象的初始化方法。 */ public MyException(){ super(); } public MyException(String message) { super(message);// 如果自定义异常需要异常信息,可以通过调用父类的带有字符串参数的构造函数即可。 } } ~~~ * 自定义异常继承RuntimeException演示 ~~~ class MyException extends RuntimeException{ /* 为什么要定义构造函数,因为看到Java中的异常描述类中有提供对异常对象的初始化方法。 */ MyException(){ super(); } MyException(String message) { super(message);// 如果自定义异常需要异常信息,可以通过调用父类的带有字符串参数的构造函数即可。 } } ~~~ # assert断言 程序执行到某行代码处一定是预期的结果 默认情况下断言不应该影响程序的运行,也就是说在java解释程序的运行时候断言不起作用 启用断言: `java -ea TestDemo` ~~~ int num = 20; assert num == 10: "num不是10"; ~~~ java的断言设计要比c++强很多,它不会影响程序的执行 # try-with-resource关闭资源 JDK1.7中 ## 简介 我们知道,在Java编程过程中,如果打开了外部资源(文件、数据库连接、网络连接等),我们必须在这些外部资源使用完毕后,手动关闭它们。因为外部资源不由JVM管理,无法享用JVM的垃圾回收机制,如果我们不在编程时确保在正确的时机关闭外部资源,就会导致外部资源泄露,紧接着就会出现文件被异常占用,数据库连接过多导致连接池溢出等诸多很严重的问题。 ## 传统的资源关闭方式 为了确保外部资源一定要被关闭,通常关闭代码被写入finally代码块中,当然我们还必须注意到关闭资源时可能抛出的异常,于是变有了下面的经典代码: ~~~ public static void main(String[] args) { FileInputStream inputStream = null; try { inputStream = new FileInputStream(new File("test")); System.out.println(inputStream.read()); } catch (IOException e) { throw new RuntimeException(e.getMessage(), e); } finally { if (inputStream != null) { try { inputStream.close(); } catch (IOException e) { throw new RuntimeException(e.getMessage(), e); } } } } ~~~ 熟悉其他语言的朋友可能会开始吐槽了,在C++中,我们可以把关闭资源的代码放在析构函数中,在C#中,我们有using代码块。这些语法都有一个共同的特性,让外部资源的关闭行为与外部资源的句柄对象的生命周期关联,当外部资源的句柄对象生命周期终结时(例如句柄对象已出作用域),外部资源的关闭行为将被自动调用。这样不仅更加符合面向对象的编程理念(将关闭外部资源的行为内聚在外部资源的句柄对象中),也让代码更加简洁易懂。怎么到了Java这里,就找不到自动关闭外部资源的语法特性了呢。 ## try-with-resource语法 确实,在JDK7以前,Java没有自动关闭外部资源的语法特性,直到JDK7中新增了try-with-resource语法,才实现了这一功能。 那什么是try-with-resource呢?简而言之,当一个外部资源的句柄对象(比如FileInputStream对象)实现了AutoCloseable接口,那么就可以将上面的板式代码简化为如下形式: ~~~ public static void main(String[] args) { try (FileInputStream inputStream = new FileInputStream(new File("test"))) { System.out.println(inputStream.read()); } catch (IOException e) { throw new RuntimeException(e.getMessage(), e); } } ~~~ 将外部资源的句柄对象的创建放在try关键字后面的括号中,当这个try-catch代码块执行完毕后,Java会确保外部资源的close方法被调用。代码是不是瞬间简洁许多! ## 实现原理 try-with-resource并不是JVM虚拟机的新增功能,只是JDK实现了一个语法糖,当你将上面代码反编译后会发现,其实对JVM虚拟机而言,它看到的依然是之前的写法: ~~~ public static void main(String[] args) { try { FileInputStream inputStream = new FileInputStream(new File("test")); Throwable var2 = null; try { System.out.println(inputStream.read()); } catch (Throwable var12) { var2 = var12; throw var12; } finally { if (inputStream != null) { if (var2 != null) { try { inputStream.close(); } catch (Throwable var11) { var2.addSuppressed(var11); } } else { inputStream.close(); } } } } catch (IOException var14) { throw new RuntimeException(var14.getMessage(), var14); } } ~~~ ## 异常抑制 通过反编译的代码,大家可能注意到代码中有一处对异常的特殊处理: ~~~ var2.addSuppressed(var11); ~~~ 这是try-with-resource语法涉及的另外一个知识点,叫做异常抑制。当对外部资源进行处理(例如读或写)时,如果遭遇了异常,且在随后的关闭外部资源过程中,又遭遇了异常,那么你catch到的将会是对外部资源进行处理时遭遇的异常,关闭资源时遭遇的异常将被“抑制”但不是丢弃,通过异常的getSuppressed方法,可以提取出被抑制的异常。 ## 总结 1. **当一个外部资源的句柄对象实现了AutoCloseable接口或者他的子接口Closeable,JDK7中便可以利用try-with-resource语法更优雅的关闭资源,消除板式代码** 2. try-with-resource时,如果对外部资源的处理和对外部资源的关闭均遭遇了异常,“关闭异常”将被抑制,“处理异常”将被抛出,但“关闭异常”并没有丢失,而是存放在“处理异常”的被抑制的异常列表 3. 可以在一条try语句中管理多个资源,每个资源以`";"`隔离开来 ## jdk9改进 现在用资源语句编写 try 将更容易。 以前,执行后必须关闭的所有资源必须在 try 子句中初 始化,如下例所示: 从 Java 9 开始,我们可以在 try 子句中使用最终和有效的最终资源: ~~~ Resource res1 = new Resource(); Resource res2 = new Resource(); try (res1; res2) { } ~~~