十二、异常
12.1 什么是异常
异常是指中断程序正常执行的一个不确定的事件。
当异常发生时,程序的正常执行流程就会被打断,终会导致 JVM 的非正常停止。 一般情况下,程序都会有很多条语句,如果没有异常处理机制,前面的语句一旦出现了异常,后面的语句就没办法继续执行了。
有了异常处理机制后,程序在发生异常的时候就不会中断,我们可以对异常进行捕获,然后改变程序执行的流程。除此之外,异常处理机制可以保证我们向用户提供友好的提示信息,而不是程序原生的异常信息(这些信息用户根本理解不了的)。
在 Java 等面向对象的编程语言中,异常本身是一个类,产生异常就是创建异常对象并抛出了一个异常对象。
那么,导致程序抛出异常的原因有哪些呢?
- 程序试图在打开一个不存在的文件;
- 程序遇到了网络连接问题;
- 用户输入了糟糕的数据;
- 程序在处理算术问题是没有考虑除数为 0 的情况;
- …
注:异常指的并不是语法错误,如果语法错了,编译是不通过的,不会产生字节码文件,根本不能运行。
12.2 异常体系
异常机制其实是帮助我们找到程序中的问题。异常的根类是 java.lang.Throwable ,其下有两个子类: java.lang.Error 与 java.lang.Exception,平常所说的异常指 java.lang.Exception 。
Throwable 体系:
- Error:严重错误 Error,无法通过处理的错误,只能事先避免。好比绝症。
- Exception:表示异常,异常产生后程序员可以通过代码的方式纠正,使程序继续运行,是必须要处理的。好比感冒、阑尾炎。
Throwable 中的常用方法:
-
public void printStackTrace() :打印异常的详细信息。包含了异常的类型、异常的原因、还包括异常出现的位置,在开发和调试阶段,都得使用 printStackTrace。
-
public String getMessage() :获取发生异常的原因。提示给用户的时候,就提示错误原因。
-
public String toString() :获取异常的类型和异常描述信息(不用)。
public class exception { public static void main(String[] args) { int[] arr = {1,2,3}; try { System.out.println(arr[3]); }catch (Exception ex){ /** * java.lang.ArrayIndexOutOfBoundsException: 3 * at com.qhj.Throwable.exception.main(exception.java:7) */ ex.printStackTrace(); System.out.println(ex.getMessage());//3 } } }
Exception 和 Error 的区别
error 为错误,exception 为异常,错误的等级比异常要高一些。
Error 的出现,意味着程序出现了严重的问题,而这些问题不应该再交给 Java 的异常处理机制来处理,程序应该直接崩溃掉。
比如说 OutOfMemoryError,内存溢出了,这就意味着程序在运行时申请的内存大于系统能够提供的内存而导致出现的错误,这种错误的出现,对程序来说是致命的。
Exception 的出现,意味着程序出现了一些在可控范围内的问题,我们应当采取措施进行挽救。
比如说 ArithmeticException,因为除数出现了 0 的情况,我们可以选择捕获异常,然后提示用户不应该进行除 0 操作。当然,这里更好的做法是直接对除数进行判断:如果是 0 就不进行除法运算,而是告诉用户换一个非 0 的数进行运算。
12.3 异常分类—checked 和 unchecked 异常
- 编译时期异常:checked 异常。在编译时期就会检查,如果没有处理异常,则编译失败(如日期格式化异常)。
- 运行时期异常:runtime 异常(也叫 unchecked 异常)。在运行时期检查异常,通常是可以通过编码进行规避的,并不需要显示地捕获或者抛出(如数学异常)。
Throwable 类:是所有异常类的根类,所有的异常类都是由它继承。换句话说,只有 Throwable 类(或者子类)的对象才能使用 throw 关键字抛出,或者作为 catch 的参数类型。
Error:表示错误是不能处理的,因为这是系统内部的错误,运行时报错,是系统问题。
Exception:是程序员可以根据问题描述来处理的。又分为编译时异常(异常必须处理)
和运行时异常(异常可以处理,但是不一定处理,一般不处理)
两大类。
像 IOException、ClassNotFoundException、SQLException 都属于 checked 异常;像 RuntimeException 以及子类 ArithmeticException、ClassCastException、ArrayIndexOutOfBoundsException、NullPointerException,都属于 unchecked 异常。
unchecked 异常可以不在程序中显示处理,就像之前提到的 ArithmeticException;但 checked 异常必须显式处理。
举个例子:
Class clz = Class.forName("com.qhj.test");
如果没做处理的话,在 idea 环境下就会提示你:这行代码可能会抛出异常 java.lang.ClassNotFoundException。
这种情况下有两种办法:
-
使用 try-catch 进行捕获
try { Class clz = Class.forName("com.qhj.test"); } catch (ClassNotFoundException e) { e.printStackTrace(); }
注意这里打印异常堆栈信息的 printStackTrace() 方法,该方法会将异常的堆栈信息打印到标准的控制台下。如果是测试环境,这样的写法是允许的;但是在生产环境中这样的写法是不可取的,必须使用日志框架把异常的堆栈信息输出到日志系统中,否则可能没有办法去跟踪。
-
在方法签名上使用 throws 关键字抛出
public class ExceptionTest1 { public static void main(String[] args) throws ClassNotFoundException { Class clz = Class.forName("com.qhj.test"); } }
这样做的好处是不需要对异常进行捕获处理,只需要交给 Java 虚拟机来处理即可;坏处就是没办法针对这种情况做相应的处理。
01、NoClassDefFoundError 和 ClassNotFoundException 有什么区别?
这是面试中经常被问到的问题:NoClassDefFoundError 和 ClassNotFoundException 有什么区别?
它们都是由于系统运行时找不到要加载的类导致的,但是触发的原因不一样:
- NoClassDefFoundError:程序在编译时可以找到所依赖的类,但是在运行时找不到指定的类文件,导致抛出该错误。原因可能是 jar 包缺失或者调用了初始化失败的类。
- ClassNotFoundException:当动态加载 Class 对象的时候找不到对应的类时抛出该异常。原因可能是要加载的类不存在或者类名写错了。
02、Java 中的 checked 有必要吗?
很多帖子上说 Java 中的 checked 很没有必要,这种异常在编译期要么 try-catch,要么 throws,但是又不一定会出现异常,那这样的设计有意义吗?
的确,它假设我们捕获了异常,并且针对这种情况做了相应的处理。但是有时候根本就没办法处理。像 ClassNotFoundException 异常,假设对其进行了 try-catch,可是当真的出现了 ClassNotFoundException 异常后,我们也没多少的可操作性了。
并且,checked 异常也不兼容函数式编程,这在写了 Lambda/Stream 代码的时候就会体会到的。
但是,checked 也并非一无是处,尤其是在遇到 IO 或者网络异常的时候。比如进行 Socket 连接:
public class test2 {
private String mHost;
private int mPort;
private Socket mSocket;
private final Object mLock = new Object();
public void run() {
}
private void initSocket() {
while (true) {
try {
Socket socket = new Socket(mHost, mPort);
synchronized (mLock) {
mSocket = socket;
}
break;
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
大概意思就是:当发生 IOException 异常的时候,socket 就重新尝试连接,否则就跳出循环。
这就意味着如果 IOException 不是 checked 异常,这种写法就略显突兀了。因为 IOException 没办法像 ArithmeticException 那样用一个 if 语句去判断除数是否为 0 来规避。
或者说,强制性的 checked 异常可以让我们在编程时去思考,遇到这种异常的时候该怎么更优雅的去处理。
显然,Socket 编程中,一定会遇到 IOException 异常的,假如 IOException 是 unchecked 类型的异常,就意味着开发者也可以不用考虑该怎么处理,而是直接跳过,交给 Java 虚拟机来处理,显然这样是不合适的。
12.4 异常的产生过程解析
public class ArrayTools {
// 对给定的数组通过给定的角标获取元素。
public static int getElement(int[] arr, int index) {
int element = arr[index];
return element;
}
}
public class ExceptionDemo {
public static void main(String[] args) {
int[] arr = { 34, 12, 67 };
intnum = ArrayTools.getElement(arr, 4)
System.out.println("num=" + num);
System.out.println("over");
}
}
12.5 异常的声明和抛出
01、异常的声明(throws)
在 Java 中,当前执行的语句必须属于某个方法,Java 解释器调用 main() 方法开始执行程序。如果方法中存在检查异常,若不对其进行捕获,就必须在方法头中显式声明该异常,以便告知方法的调用者此方法有异常,需要进行处理。在方法中声明一个异常,方法头中使用关键字 throws,后面接上要声明的异常。如果声明多个异常,就使用逗号分隔开:
public static void method() throws IOException, FileNotFoundException{
//something statements
}
注意:如果父类的方法没有声明异常,子类继承方法后,也不能声明异常。
通常地,应该捕获那些知道如何处理的异常,将不知道如何处理的异常继续传递下去。传递异常可以在方法签名处使用 throws 关键字声明可能会抛出的异常:
private static void readFile(String filePath) throws IOException {
File file = new File(filePath);
String result;
BufferedReader reader = new BufferedReader(new FileReader(file));
while((result = reader.readLine())!=null) {
System.out.println(result);
}
reader.close();
}
Throws 抛出异常的规则:
- 如果是不可查的异常(unchecked exception),即 Error、RuntimeException 或它们的子类,那么可以不使用 throws 关键字来声明要抛出的异常,编译仍能顺利通过,但在运行时会被系统抛出。
- 必须声明方法可抛出的任何可查异常(checked exception)。即如果一个方法可能出现受可查异常,要么用 try-catch 语句捕获,要么用 throws 子句声明将它抛出,否则会导致编译错误仅当抛出了异常,该方法的调用者才必须处理或者重新抛出该异常。
- 当方法的调用者无力处理该异常的时候,应该继续抛出,而不是囫囵吞枣。调用方法必须遵循任何可查异常的处理和声明规则。若覆盖一个方法,则不能声明与覆盖方法不同的异常。声明的任何异常必须是被覆盖方法所声明异常的同类或子类。
02、异常的抛出(throw)
如果代码可能会引发某种错误,可以创建一个合适的异常类实例并抛出它,这就是抛出异常:
public static double method(int value) {
if(value == 0) {
throw new ArithmeticException("参数不能为0"); // 抛出一个运行时异常
}
return 5.0 / value;
}
大部分情况下都不需要手动抛出异常,因为 Java 的大部分方法要么已经处理异常,要么已经声明异常。所以一般都是捕获异常或者再往上抛。
有时我们会从 catch 中抛出一个异常,目的是为了改变异常的类型。多用于在多系统集成时,当某个子系统故障,异常类型可能有多种,可以用统一的异常类型向外暴露,不需要暴露太多内部异常细节:
private static void readFile(String filePath) throws MyException {
try {
// code
} catch (IOException e) {
MyException ex = new MyException("read file failed.");
ex.initCause(e);
throw ex;
}
}
03、关于 throw 和 throws
throw 关键字用于主动地抛出异常。
正常情况下,当除数为 0 的时候,程序会主动抛出 ArithmeticException 异常。但是如果我们想要实现当除数为 1 的时候也抛出 ArithmeticException 异常,就可以使用 throw 关键字主动地抛出异常。
throw 用在方法内,用来抛出一个异常对象,将这个异常对象传递到调用者处,并结束当前方法的执行。语法就是在 throw 关键字后跟上 new 关键字,以及异常的类型、参数就可以了:
throw new Exception("error message!");
举个例子:
/**
* @author QHJ
* @date 2022/11/10 09:33
* @description: ExceptionTest2
*/
public class ExceptionTest2 {
public static void main(String[] args) {
checkAge(10);
System.out.println("Have a good time!");
}
static void checkAge(int age) {
if (age < 18) {
throw new ArithmeticException("未成年禁止入内!");
} else {
System.out.println("请进!");
}
}
}
程序在运行时就会抛出以下错误:
throws 关键字的作用就和 throw 完全不同。
throws 关键字用于声明异常,将问题标识出来,报告给调用者。
如果方法内通过 throw 抛出了编译时异常,而没有捕获处理,那么必须通过 throws 进行声明,让调用者去处理。
关键字 throws 运用于方法声明上,用于表示当前方法不处理异常,而是提醒该方法的调用者来处理异常(抛出异常)。
对于检查型异常(checked 异常)来说,如果你没有做处理,编译器就会给出提示:
那么,什么情况下使用 throws,什么情况下使用 try-catch 呢?
假设现在有一个方法 myMethod(),可能会出现 ArithmeticException 异常,也可能会出现 NullPointerException。这种情况下,可以使用 try-catch 来处理:
public void myMethod() {
try {
// 可能抛出异常
} catch (ArithmeticException e) {
// 算术异常
} catch (NullPointerException e) {
// 空指针异常
}
}
但是如果有好几个类似的 myMethod() 方法,如果为每个方法都加上 try-catch,就会显得非常繁琐,代码会变得很长、可读性特别差。
所以,在这种情况下,更推荐使用 throws 关键字。在方法签名上声明可能会抛出的异常,然后在调用该方法的地方使用 try-catch 进行处理:
public static void main(String args[]){
try {
myMethod1();
} catch (ArithmeticException e) {
// 算术异常
} catch (NullPointerException e) {
// 空指针异常
}
}
public static void myMethod1() throws ArithmeticException, NullPointerException{
// 方法签名上声明异常
}
throw 和 throws 的区别:
-
throws 关键字用于声明异常,它的作用和 try-catch 相似;而 throw 关键字用于显式地抛出异常。
-
throws 关键字后面跟的是异常的名字;而 throw 关键字后面跟的是异常的对象。
throws ArithmeticException; throw new ArithmeticException("算术异常");
-
throws 关键字出现在方法签名上,而 throw 关键字出现在方法体里。
-
throws 关键字在声明异常的时候跟多个,用逗号隔开;而 throw 关键字每次只能抛出一个异常。
12.6 异常的捕获
Java异常捕获有五个关键字:try、catch、finally、throw、throws。
01、try-catch
在一个 try-catch 语句块中可以捕获多个异常类型,并针对不同类型的异常做出不同的处理:
try{
// 编写可能会出现异常的代码
}catch(异常类型1 e1){
// 处理异常的代码
// 记录日志/打印异常信息/继续抛出异常
}catch (异常类型2 e2){
// 处理异常的代码
// 记录日志/打印异常信息/继续抛出异常
}
public class tryCatchDemo {
public static void main(String[] args) {
// 当有异常产生时,必须处理,要么捕获要么声明
try {
read("b.txt");
} catch (FileNotFoundException e) {
// try 中抛出的是什么异常,在括号中就定义什么异常类型
e.printStackTrace();
}
System.out.println("over!");
}
/**
* 此方法中有编译时异常
* @param path
* @throws FileNotFoundException
*/
public static void read(String path) throws FileNotFoundException {
if (!path.equals("a.txt")){
throw new FileNotFoundException("文件不存在!");
}
}
}
注意:
try 和 catch 都不能单独使用,必须连用。
另外,如果一些代码确定不会抛出异常,就尽量不要把它包裹在 try 块里,因为加了异常处理的代码执行起来要比没有加的花费更多的时间。
一个 try 块后面可以跟多个 catch 块,用来捕获不同类型的异常并做相应的处理。
当 try 块中的某一行代码发生异常时,之后的代码就不再执行,而是会跳转到异常对应的 catch 块中执行。
如果一个 try 块后面跟了多个与之关联的 catch 块,那么应该把特定的异常放在前面,通用型的异常放在后面,不然编译器会提示错误
:
static void test() {
int num1, num2;
try {
num1 = 0;
num2 = 62 / num1;
System.out.println(num2);
System.out.println("try 块的最后一句");
} catch (ArithmeticException e) {
// 算术运算发生时跳转到这里
System.out.println("除数不能为零");
} catch (Exception e) {
// 通用型的异常意味着可以捕获所有的异常,它应该放在最后面,
System.out.println("异常发生了");
}
System.out.println("try-catch 之外的代码.");
}
为什么 Exception 不能放到 ArithmeticException 前面呢?
因为 ArithmeticException 是 Exception 的子类,它更具体,我们看到的这个异常很明显是发生了算数错误,而 Exception 比较广泛,它隐藏了具体的异常信息,我们看到后并不确定到底是发生了哪一种类型的异常,非常不利于对错误的排查。
再者,如果把通用型的异常放在前面,就意味着其他的 catch 块永远也不会执行,所以编译器就直接提示错误了。
举个例子:
static void test1 () {
try{
int arr[]=new int[7];
arr[4]=30/0;
System.out.println("try 块的最后");
} catch(ArithmeticException e){
System.out.println("除数必须是 0");
} catch(ArrayIndexOutOfBoundsException e){
System.out.println("数组越界了");
} catch(Exception e){
System.out.println("一些其他的异常");
}
System.out.println("try-catch 之外");
}
这段代码在执行的时候,第一个 catch 块会执行,因为除数为零。将代码改动一下:
static void test1 () {
try{
int arr[]=new int[7];
arr[9]=30/1;
System.out.println("try 块的最后");
} catch(ArithmeticException e){
System.out.println("除数必须是 0");
} catch(ArrayIndexOutOfBoundsException e){
System.out.println("数组越界了");
} catch(Exception e){
System.out.println("一些其他的异常");
}
System.out.println("try-catch 之外");
}
显然,第二个 catch 块会执行,因为没有发生算数异常,但是数组越界了。再次改动代码:
static void test1 () {
try{
int arr[]=new int[7];
arr[9]=30/1;
System.out.println("try 块的最后");
} catch(ArithmeticException | ArrayIndexOutOfBoundsException e){
System.out.println("除数必须是 0");
}
System.out.println("try-catch 之外");
}
由此可见,当有多个 catch 的时候,也可以放在一起,用竖线 | 隔开。
02、finally
finally 有一些特定的代码无论异常是否发生,都需要执行。
另外,因为异常会引发程序跳转,导致有些语句执行不到。而 finally 就是解决这个问题的,在 finally 代码块中存放的代码都是一定会被执行的。
什么时候的代码必须终执行?
在没有 try-with-resource 之前,finally 块常用来关闭一些连接资源。当我们在 try 语句块中打开了一些物理资源(socket、数据库连接、IO 输入输出流等),我们都得在使用完之后,终关闭打开的资源:
OutputStream osf = new FileOutputStream( "filename" );
OutputStream osb = new BufferedOutputStream(opf);
ObjectOutput op = new ObjectOutputStream(osb);
try{
output.writeObject(writableObject);
} finally{
op.close();
}
finally 的语法:
try{
// 编写可能会出现异常的代码
}catch(异常类型 e){
// 处理异常的代码
// 记录日志/打印异常信息/继续抛出异常
}finally{
// finally 里的代码块一定会被执行
}
举个例子:
public class trycatchFinallyDemo {
public static void main(String[] args) {
/**
* 当有异常产生时,必须处理,要么捕获要么声明
*/
try {
read("b.txt");
} catch (FileNotFoundException e) {
// 抓取到的是编译期异常 抛出去的是运行期
e.printStackTrace();
}finally {
System.out.println("无论程序怎么,这里的代码都会执行!");
}
System.out.println("over!");
}
/**
* 此方法中有编译时异常
* @param path
* @throws FileNotFoundException
*/
public static void read(String path) throws FileNotFoundException {
if (!path.equals("a.txt")){
throw new FileNotFoundException("文件不存在!");
}
}
}
注意:
- try…catch…finally:自身需要处理异常,终还得关闭资源。
- 当只有在 try 或者 catch 中调用退出 JVM 的相关方法,此时 finally 才不会执行,否则 finally 永远会执行。
- finally 不能单独使用。
使用 finally 块的时候需要遵守这些规则
:
- finally 块前面必须有 try 块,不要把 finally 块单独拉出来使用(编译器也不允许这样做)。
- finally 块不是必选项,有 try 块的时候不一定要有 finally 块。
- 如果 finally 块中的代码可能会发生异常,也应该使用 try-catch 进行包裹。
- 即便是 try 块中执行了 return、break、continue 这些跳转语句,finally 块也会被执行。
会不会有不执行 finally 的情况呢?——有。
- 遇到了死循环;
- 执行了 System.exit() 。
- finally 语句块中发生了异常;
- 程序所在的线程死亡;
- 关闭 CPU。
System.exit()
和 return 语句不同,前者是用来退出程序的,后者只是回到了上一级方法调用。参数 status 的值用来设置状态的:如果是异常退出,设置为非 0 即可,通常用 1 来表示;如果是想正常退出程序,用 0 表示即可。
03、try-catch-finally
-
常规语法
try { // 执行程序代码,可能会出现异常 } catch(Exception e) { // 捕获异常并处理 } finally { // 一定会执行的代码 }
-
执行的顺序
- 当 try 没有捕获到异常时:try 语句块中的语句逐一被执行,程序将跳过 catch 语句块,执行 finally 语句块和其后的语句;
- 当 try 捕获到异常时,catch 语句块里没有处理此异常的情况:当 try 语句块里的某条语句出现异常时,而没有处理此异常的 catch 语句块时,此异常将会抛给 JVM 处理,finally 语句块里的语句还是会被执行,但是 finally 语句块后的语句不会被执行;
- 当 try 捕获到异常,catch 语句块里有处理此异常的情况:在 try 语句块中是按照顺序来执行的,当执行到某一条语句出现异常时,程序将跳到 catch 语句块,并与 catch 语句块逐一匹配,找到与之对应的处理程序,其他的 catch 语句块将不会被执行,而 try 语句块中,出现异常之后的语句也不会被执行,catch 语句块执行完后,执行 finally 语句块里的语句,最后执行 finally 语句块后的语句。
04、try-finally
可以直接使用 try-finally 吗?可以。
try 块中引起异常,异常代码之后的语句不再执行,直接执行 finally 语句。try 块没有引发异常,则执行完 try 块就执行 finally 语句。
try-finally 可用在不需要捕获异常的代码,可以保证资源在使用后被关闭。例如 IO 流中执行完相应操作后,关闭相应资源;使用 Lock 对象保证线程同步,通过 finally 可以保证锁会被释放;数据库连接代码时,关闭连接操作等等:
// 以Lock加锁为例
ReentrantLock lock = new ReentrantLock();
try {
// 需要加锁的代码
} finally {
lock.unlock(); // 保证锁一定被释放
}
05、try-with-resource
try-with-resource 是 JDK 7 中引入的,很容易被忽略。
在上面的方式中,finally 中的 close() 方法也可能抛出 IOException,从而覆盖了原始异常。JDK 7 提供了更优雅的方式来实现资源的自动释放,自动释放的资源需要是实现了 AutoCloseable 接口的类:
private static void tryWithResourceTest(){
try (Scanner scanner = new Scanner(new FileInputStream("c:/abc"),"UTF-8")){
// code
} catch (IOException e){
// handle exception
}
}
扒一下 Scanner:
public final class Scanner implements Iterator<String>, Closeable {
// ...
}
public interface Closeable extends AutoCloseable {
public void close() throws IOException;
}
try 代码块退出时,会自动调用 scanner.close() 方法,和把 scanner.close() 方法放在 finally 代码块中。不同的是,如果 scanner.close() 抛出异常,则会被抑制,抛出的仍然为原始异常。被抑制的异常会由 addSusppressed() 方法添加到原来的异常,如果想要获取被抑制的异常列表,可以调用 getSuppressed() 方法来获取。
12.7 自定义异常
在开发中根据自己业务的异常情况来定义异常类.
自定义一个业务逻辑异常: RegisterException。一个注册异常类。
异常类的定义:
- 自定义一个编译期异常: 自定义类并继承于 java.lang.Exception 。
- 自定义一个运行时期的异常类:自定义类并继承于 java.lang.RuntimeException 。
举个例子:
// 业务逻辑异常
public class RegisterException extends Exception {
/**
* 空参构造
*/
public RegisterException() { }
/**
*
* @param message 表示异常提示
*/
public RegisterException(String message) {
super(message);
}
}
public class Demo {
// 模拟数据库中已存在账号
private static String[] names = {"bill","hill","jill"};
public static void main(String[] args) {
// 调用方法
try{
// 可能出现异常的代码
checkUsername("nill");
System.out.println("注册成功");// 如果没有异常就是注册成功
}catch(RegisterException e){
// 处理异常
e.printStackTrace();
}
}
// 判断当前注册账号是否存在
// 因为是编译期异常,又想调用者去处理,所以声明该异常
public static boolean checkUsername(String uname) throws LoginException{
for (String name : names) {
if(name.equals(uname)){
// 如果名字在这里面 就抛出登陆异常
throw new RegisterException("亲," + name + "已经被注册了!");
}
}
return true;
}
}
12.7 详解 Java 中的 try-with-resources 语法糖
看下面这段代码:
/**
* @author QHJ
* @date 2022/11/10 15:04
* @description: TrycatchfinallyDecoder
*/
public class TrycatchfinallyDecoder {
public static void main(String[] args) {
BufferedReader br = null;
try {
String path = TrycatchfinallyDecoder.class.getResource("/qhj.txt").getFile();
String decodePath = URLDecoder.decode(path, "utf-8");
br = new BufferedReader(new FileReader(decodePath));
String str = null;
while ((str = br.readLine()) != null) {
System.out.println(str);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (br != null) {
try {
br.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
这段代码的意思是,在 try 块中读取文件中的内容,并一行一行的打印到控制台。如果文件找不到或者出现 IO 读写错误,就在 catch 中获取并打印错误的堆栈信息。最后在 finally 中关闭缓冲字符读取器对象 BufferedReader,有效杜绝了资源未被关闭的情况下造成的严重性能后果。
在 JDK 1.7 之前,try-catch-finally 的确是确保资源会被及时关闭的最佳方法,无论程序是否会抛出异常。
但是,这段代码还是有些臃肿的,尤其是 finally 中的代码。并且,try-catch-finally 自始至终都存在一个严重的隐患:try 中的 br.readLine() 有可能会抛出 IOException,finally 中的 br.close() 也有可能会抛出 IOException。假如两处都不幸地抛出了 IOException,那程序的调试任务就变得复杂了起来,到底是哪一处出了错误,这就需要花一番功夫去查的。
举个例子:
自定义一个类 MyfinallyReadLineThrow,它有两个方法 close() 和 readLine(),方法体都是主动抛出异常。
/**
* @author QHJ
* @date 2022/11/10 15:14
* @description: MyfinallyReadLineThrow
*/
public class MyfinallyReadLineThrow {
public void close() throws Exception {
throw new Exception("close");
}
public void readLine() throws Exception {
throw new Exception("readLine");
}
}
然后在 main() 方法中使用 try-catch-finally 的方式调用 MyfinallyReadLineThrow 的 readLine() 和 close() 方法:
/**
* @author QHJ
* @date 2022/11/10 15:17
* @description: TryfinallyCustomReadLineThrow
*/
public class TryfinallyCustomReadLineThrow {
public static void main(String[] args) throws Exception {
MyfinallyReadLineThrow myThrow = null;
try {
myThrow = new MyfinallyReadLineThrow();
myThrow.readLine();
} finally {
myThrow.close();
}
}
}
程序运行结果如下:
会发现,readLine() 方法的异常信息被 close() 方法的堆栈信息吞了!这样就会让我们误以为要调查的目标是 close() 方法而不是 readLine() 方法——尽管它也是应该被怀疑的对象。
但是有了 try-with-resources 后,这些问题就迎刃而解了。前提条件只有一个:就是需要释放的资源(比如 BufferedReader)实现了 AutoCloseable 接口
:
/**
* @author QHJ
* @date 2022/11/10 15:23
* @description: TryWithResourcesThrow
*/
public class TryWithResourcesThrow {
public static void main(String[] args) {
try {
String path = TrycatchfinallyDecoder.class.getResource("/qhj.txt").getFile();
String decodePath = URLDecoder.decode(path, "utf-8");
try(BufferedReader br = new BufferedReader(new FileReader(decodePath))){
String str = null;
while ((str = br.readLine()) != null) {
System.out.println(str);
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
这里不再使用 finally 块了,取而代之的是把要释放的资源写在 try 后的 () 中。如果有多个资源需要释放的话,可以直接在 () 中添加。
如果想要释放自定义资源的话,只需要让它实现 AutoCloseable 接口,并提供 close() 方法即可:
/**
* @author QHJ
* @date 2022/11/10 15:35
* @description: TrywithresourcesCustom
*/
public class TrywithresourcesCustom {
public static void main(String[] args) {
try (MyResource myResource = new MyResource()) {
} catch (Exception e) {
e.printStackTrace();
}
}
}
class MyResource implements AutoCloseable {
@Override
public void close() throws Exception {
System.out.println("关闭自定义资源!");
}
}
程序运行结果:
这里只是在 try() 中 new 了一个 MyResource 的对象,其他什么也没干,close() 方法就执行了。扒一下反编译后的字节码:
class MyResource implements AutoCloseable {
MyResource() {
}
public void close() throws Exception {
System.out.println("关闭自定义资源!");
}
}
public class TrywithresourcesCustom {
public TrywithresourcesCustom() {
}
public static void main(String[] args) {
try {
MyResource myResource = new MyResource();
Object var2 = null;
if (myResource != null) {
if (var2 != null) {
try {
myResource.close();
} catch (Throwable var4) {
((Throwable)var2).addSuppressed(var4);
}
} else {
myResource.close();
}
}
} catch (Exception var5) {
var5.printStackTrace();
}
}
}
我们会发现,编译器主动为 try-with-resources 进行了改造:在 try 中调用了 close() 方法。
接下来我们在 MyResource 类中再添加一个 out() 方法,并在 try 块中调用它:
/**
* @author QHJ
* @date 2022/11/10 15:35
* @description: TrywithresourcesCustom
*/
public class TrywithresourcesCustom {
public static void main(String[] args) {
try (MyResource myResource = new MyResource()) {
myResource.out();
} catch (Exception e) {
e.printStackTrace();
}
}
}
class MyResource implements AutoCloseable {
@Override
public void close() throws Exception {
System.out.println("关闭自定义资源!");
}
public void out() throws Exception{
System.out.println("青花椒,一枚有趣的程序员");
}
}
程序运行结果:
再来扒一下反编译后的代码:
class MyResource implements AutoCloseable {
MyResource() {
}
public void close() throws Exception {
System.out.println("关闭自定义资源!");
}
public void out() throws Exception {
System.out.println("青花椒,一枚有趣的程序员");
}
}
public class TrywithresourcesCustom {
public TrywithresourcesCustom() {
}
public static void main(String[] args) {
try {
MyResource myResource = new MyResource();
Throwable var2 = null;
try {
myResource.out();
} catch (Throwable var12) {
var2 = var12;
throw var12;
} finally {
if (myResource != null) {
if (var2 != null) {
try {
myResource.close();
} catch (Throwable var11) {
var2.addSuppressed(var11);
}
} else {
myResource.close();
}
}
}
} catch (Exception var14) {
var14.printStackTrace();
}
}
}
由此可见,catch 块主动调用了 myResource.close(),并且有一个很关键的操作 var2.addSuppressed(var11)
。
当一个异常被抛出的时候,可能有其他异常因为该异常而被抑制住,从而无法正常抛出。这时可以通过 addSuppressed() 方法把这些被抑制的方法记录下来,然后被抑制的异常就会出现在抛出的异常的堆栈信息中,可以通过 getSuppressed() 方法来获取这些异常。这样做的好处是不会丢失任何异常,方便我们进行调试。
回到刚刚那个例子——在 try-catch-finally 中,readLine() 方法的异常信息竟然被 close() 方法的堆栈信息吞了。现在有了 try-with-resources,再来看看和 readLine() 方法一致的 out() 方法会不会被 close() 被吞掉:
/**
* @author QHJ
* @date 2022/11/10 15:14
* @description: MyfinallyReadLineThrow
*/
public class MyfinallyReadLineThrow implements AutoCloseable {
@Override
public void close() throws Exception {
throw new Exception("close");
}
public void readLine() throws Exception {
throw new Exception("readLine");
}
}
调用这两个方法:
/**
* @author QHJ
* @date 2022/11/10 15:50
* @description: TrywithresourcesCustomOutThrow
*/
public class TrywithresourcesCustomOutThrow {
public static void main(String[] args) {
try (MyResourceOutThrow resource = new MyResourceOutThrow();) {
resource.out();
} catch (Exception e) {
e.printStackTrace();
}
}
}
程序运行结果:
显然,out() 的异常堆栈信息打印出来了,并且 close() 方法的堆栈信息上加了一个关键字 Suppressed(抑制)。
也就是说所,在处理必须关闭的资源时,始终优先考使用 try-with-resources,而不是 try–catch-finally。前者产生的代码更加简洁、清晰,产生的异常信息也更靠谱。