ToB企服应用市场:ToB评测及商务社交产业平台

标题: Java异常详解(全文干货) [打印本页]

作者: 九天猎人    时间: 2024-8-28 20:11
标题: Java异常详解(全文干货)
介绍


Throwable

Throwable 是 Java 语言中全部错误与异常的超类。
Throwable 包含两个子类:Error(错误)和 Exception(异常),它们通常用于指示发生了异常环境。
Throwable 包含了其线程创建时线程执行堆栈的快照,它提供了 printStackTrace() 等接口用于获取堆栈跟踪数据等信息。
Error(错误)

Error 类及其子类:步伐中无法处理的错误,体现运行应用步伐中出现了严重的错误。
此类错误一般体现代码运行时 JVM 出现问题。通常有
此类错误发生时,JVM 将终止线程。这些错误是不受检异常,非代码性错误。因此,当此类错误发生时,应用步伐不应该去处理此类错误。按照Java惯例,我们是不应该实现任何新的Error子类的!
Exception(异常)

步伐本身可以捕获并且可以处理的异常。Exception 这种异常又分为两类:运行时异常和编译时异常。
常见的异常

在Java中提供了一些异常用来描述经常发生的错误,对于这些异常,有的需要步伐员进行捕获处理或声明抛出,有的是由Java虚拟机主动进行捕获处理。Java中常见的异常类:
这些异常时Java内置的异常类,当然用户也可以根据业务自定义自己的异常类:
  1. public class MyException extends RuntimeException {
  2.     // 无参构造器
  3.     public MyException() {}
  4.     // 带有详细信息的构造器
  5.     public MyException(String message) {
  6.         super(message);
  7.     }
  8.     // 带有引起此异常的原因的构造器
  9.     public MyException(Throwable cause) {
  10.         super(cause);
  11.     }
  12.     // 同时包含错误信息和原因的构造器
  13.     public MyException(String message, Throwable cause) {
  14.         super(message, cause);
  15.     }
  16. }
复制代码
异常的处理方式

  1. if(预想的异常情况出现){   
  2.     throw new 相应的异常();//可以是自定义的异常
  3. } //还可以在括号内写上出现异常时的”输出语句“
复制代码
即:既要发现异常,又要处理异常。
别的:这种具有针对性的声明只能抛出单个异常
异常常用方法

try-catch-finally

执行次序

执行流程图如下:

也可以直接用try-finally,不使用catch,可用在不需要捕获异常的代码,可以保证资源在使用后被关闭。例如IO流中执行完相应利用后,关闭相应资源;使用Lock对象保证线程同步,通过finally可以保证锁会被释放;数据库连接代码时,关闭连接利用等等。
finally遇见如下环境不会执行:
finally 经典异常处理代码题

题目一
  1. public class Test {
  2.     public static void main(String[] args) {
  3.         System.out.println(test());
  4.     }
  5.     public static int test() {
  6.         try {
  7.             return 1;
  8.         } catch (Exception e) {
  9.             return 2;
  10.         } finally {
  11.             System.out.print("3");
  12.         }
  13.     }
  14. }
  15. //输出:
  16. 31
复制代码
try、catch、finally 的基础用法,在 return 前会先执行 finally 语句块,所以会先输出 finally 里的 3,再输出 return 的 1。由于这里try中没有异常发生,因此catch中的return不会执行
题目二
  1. public class Test {
  2.     public static void main(String[] args) {
  3.         System.out.println(test());
  4.     }
  5.     public static int test() {
  6.         try {
  7.             int i = 1/0;
  8.             return 1;
  9.         } catch (Exception e) {
  10.             return 2;
  11.         } finally {
  12.             System.out.print("3");
  13.         }
  14.     }
  15. }
  16. //输出:
  17. 32
复制代码
在 return 前会先执行 finally 语句块,所以会先输出 finally 里的 3,再输出 catch 中 return 的 2。由于这里try中有异常发生,因此try后续语句不会再执行
题目三
  1. public class Test {
  2.     public static void main(String[] args) {
  3.         System.out.println(test());
  4.     }
  5.     public static int test() {
  6.         try {
  7.             return 2;
  8.         } finally {
  9.             return 3;
  10.         }
  11.     }
  12. }
  13. //输出:
  14. 3
复制代码
try中的return前先执行 finally,结果 finally 直接 return 了,自然也就走不到 try 里面的 return 了。
题目四
  1. public class Test {
  2.     public static void main(String[] args) {
  3.         System.out.println(test());
  4.     }
  5.     public static int test() {
  6.         int i = 0;
  7.         try {
  8.             i = 2;
  9.             return i;
  10.         } finally {
  11.             i = 3;
  12.         }
  13.     }
  14. }
  15. //输出:
  16. 2
复制代码
在执行 finally 之前,JVM 会先将 i 的结果暂存起来,然后 finally 执行完毕后,会返回之前暂存的结果,而不是返回 i,所以纵然 i 已经被修改为 3,最终返回的还是之前暂存起来的结果 2。
总结

try-with-resources语法糖

配景

每当有关闭资源的需求都会使用到try-finally这个语句,比如在使用锁的时候,无论是本地的可重入锁还是分布式锁都会有下面雷同的结构代码,会在finally里面进行unlock,用于强制解锁:
  1. Lock lock = new ReentrantLock();
  2. lock.lock();
  3. try {
  4.     // doSometing
  5. } finally {
  6.     lock.unlock();
  7. }
复制代码
或者使用java的文件流读取或者写入文件的时候,也会在finally中强制关闭文件流,防止资源走漏。
  1. InputStream inputStream = new FileInputStream("file");
  2. try {
  3.     System.out.println(inputStream.read(new byte[4]));
  4. } finally {
  5.     inputStream.close();
  6. }
复制代码
其实乍一看 这样的写法应该没什么问题,但是假如出现了多个资源需要关闭我们应该怎么写呢?最常见的写法如下:
  1. InputStream inputStream = new FileInputStream("file");
  2. OutputStream outStream = new FileOutputStream("file1");
  3. try {
  4.     System.out.println(inputStream.read(new byte[4]));
  5.     outStream.write(new byte[4]);
  6. } finally {
  7.     inputStream.close();
  8.     outStream.close();
  9. }
复制代码
在外面定义了两个资源,然后在finally里面依次对这两个资源进行关闭,那么这个那里有问题呢?
问题其实在于假如在inputStream.close的时候抛出异常,那么outStream.close()就不会执行,这很明显不是想要的结果,所以后面就改成了下面这种多重嵌套的方式去写:
  1. InputStream inputStream = new FileInputStream("file");
  2. try {
  3.     System.out.println(inputStream.read(new byte[4]));
  4.     try {
  5.         OutputStream outStream = new FileOutputStream("file1");
  6.         outStream.write(new byte[4]);
  7.     } finally {
  8.         outStream.close();
  9.     }
  10. } finally {
  11.     inputStream.close();
  12. }
复制代码
在这种方式中即便是outStream.close()抛出了异常,但是依然会执行到inputStream.close(),因为他们是在不同的finally块,这个简直办理了问题,但是还有两个问题没有办理:
  1. public class CloseTest {
  2.     public void close(){
  3.         throw new RuntimeException("close");
  4.     }
  5.     public static void main(String[] args) {
  6.         CloseTest closeTest = new CloseTest();
  7.         try{
  8.             throw new RuntimeException("doSomething");
  9.         }finally {
  10.             closeTest.close();
  11.         }
  12.     }
  13. }
  14. //输出结果:Exception in thread "main" java.lang.RuntimeException: close
复制代码
上面这个代码,期望的是能抛出doSomething的这个异常,但是实际的数据结果却是close的异常,这和预期不符合。
try-with-resources如何办理的

上面介绍了两个问题,于是在java7中引入了try-with-resources的语句,只要资源实现了AutoCloseable这个接口那就可以使用这个语句了,之前的文件流已经实现了这个接口,因此可以直接使用:
  1. try (InputStream inputStream = new FileInputStream("file");
  2.     OutputStream outStream = new FileOutputStream("file1")) {
  3.     System.out.println(inputStream.read(new byte[4]));
  4.     outStream.write(new byte[4]);
  5. }
复制代码
全部的资源定义全部都在try后面的括号中进行定义,通过这种方式就可以办理上面所说的几个问题:
  1. public class CloseTest implements AutoCloseable {
  2.     @Override
  3.     public void close(){
  4.         System.out.println("close");
  5.         throw new RuntimeException("close");
  6.     }
  7.     public static void main(String[] args) {
  8.         try(CloseTest closeTest = new CloseTest();
  9.             CloseTest closeTest1 = new CloseTest();){
  10.             throw new RuntimeException("Something");
  11.         }
  12.     }
  13. }
  14. //输出结果为:
  15. close
  16. close
  17. Exception in thread "main" java.lang.RuntimeException: Something
  18.     at fudao.CloseTest.main(CloseTest.java:33)
  19.     Suppressed: java.lang.RuntimeException: close
  20.         at fudao.CloseTest.close(CloseTest.java:26)
  21.         at fudao.CloseTest.main(CloseTest.java:34)
  22.     Suppressed: java.lang.RuntimeException: close
  23.         at fudao.CloseTest.close(CloseTest.java:26)
  24.         at fudao.CloseTest.main(CloseTest.java:34)
复制代码
在代码中定义了两个CloseTest,用来验证之前close出现异常是否会影响第二个,同时在close和try块里面都抛出不同的异常,可以看见结果,输出了两个close,证明虽然close抛出异常,但是两个close都会执行。然后输出了doSomething的异常,可以发现这里输出的就是try块里面所抛出的异常,并且close的异常以Suppressed的方式记录在异常的堆栈里面,通过这样的方式两种异常都能记录下来。
异常实现原理

JVM处理异常的机制

Exception Table,称为异常表
try-catch
  1. public static void simpleTryCatch() {
  2.    try {
  3.        testNPE();
  4.    } catch (Exception e) {
  5.        e.printStackTrace();
  6.    }
  7. }
复制代码
使用javap来分析这段代码
  1. //javap -c Main
  2. public static void simpleTryCatch();
  3.     Code:
  4.        0: invokestatic  #3                  // Method testNPE:()V
  5.        3: goto          11
  6.        6: astore_0
  7.        7: aload_0
  8.        8: invokevirtual #5                  // Method java/lang/Exception.printStackTrace:()V
  9.       11: return
  10.     Exception table:
  11.        from    to  target type
  12.            0     3     6   Class java/lang/Exception
复制代码
异常表中包含了一个或多个异常处理者(Exception Handler)的信息,这些信息包含如下
当一个异常发生时,JVM处理异常的机制如下:
try-catch-finally
  1. public static void simpleTryCatchFinally() {
  2.    try {
  3.        testNPE();
  4.    } catch (Exception e) {
  5.        e.printStackTrace();
  6.    } finally {
  7.        System.out.println("Finally");
  8.    }
  9. }
复制代码
同样使用javap分析一下代码
  1. public static void simpleTryCatchFinally();
  2.     Code:
  3.       
  4.       //try 部分:
  5.       //如果有异常,则调用14位置代码,也就是catch部分代码
  6.       //如果没有异常发生,则执行输出finally操作,直至goto到41位置,执行返回操作。   
  7.        0: invokestatic  #3                  // Method testNPE:()V
  8.        3: getstatic     #6                  // Field java/lang/System.out:Ljava/io/PrintStream;
  9.        6: ldc           #7                  // String Finally
  10.        8: invokevirtual #8                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
  11.       11: goto          41
  12.       //catch部分:。如果没有异常发生,则执行输出finally操作,直至执行got到41位置,执行返回操作。
  13.       14: astore_0
  14.       15: aload_0
  15.       16: invokevirtual #5                  // Method java/lang/Exception.printStackTrace:()V
  16.       19: getstatic     #6                  // Field java/lang/System.out:Ljava/io/PrintStream;
  17.       22: ldc           #7                  // String Finally
  18.       24: invokevirtual #8                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
  19.       27: goto          41
  20.       
  21.       //finally部分的代码如果被调用,有可能是try部分,也有可能是catch部分发生异常。
  22.       30: astore_1
  23.       31: getstatic     #6                  // Field java/lang/System.out:Ljava/io/PrintStream;
  24.       34: ldc           #7                  // String Finally
  25.       36: invokevirtual #8                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
  26.       39: aload_1
  27.       40: athrow     //如果异常没有被catch捕获,而是到了这里,执行完finally的语句后,仍然要把这个异常抛出去,传递给调用处。
  28.       41: return
  29.     Exception table:
  30.        from    to  target type
  31.            0     3    14   Class java/lang/Exception
  32.            0     3    30   any
  33.           14    19    30   any
复制代码
上面的三条异常表item的意思为:
其实后两点的意思就是,无论有没有异常,finally语句块一定会被调用
try-with-resources

try-with-resources语句其实是一种语法糖,通过编译之后又回到了开始说的嵌套的那种模式:

可以发现try-with-resources被编译之后,又接纳了嵌套的模式,但是和之前的嵌套有点不同,他close的时候都使用了catch去捕获了异常,然后添加到真正的异常中,整体逻辑比之前我们自己的嵌套要复杂一些。
异常耗时

下面的测试用例简单的测试了创建对象、创建异常对象、抛出并接住异常对象三者的耗时对比:
  1. public class ExceptionTest {  
  2.   
  3.     private int testTimes;  
  4.   
  5.     public ExceptionTest(int testTimes) {  
  6.         this.testTimes = testTimes;  
  7.     }  
  8.   
  9.     public void newObject() {  
  10.         long l = System.nanoTime();  
  11.         for (int i = 0; i < testTimes; i++) {  
  12.             new Object();  
  13.         }  
  14.         System.out.println("建立对象:" + (System.nanoTime() - l));  
  15.     }  
  16.   
  17.     public void newException() {  
  18.         long l = System.nanoTime();  
  19.         for (int i = 0; i < testTimes; i++) {  
  20.             new Exception();  
  21.         }  
  22.         System.out.println("建立异常对象:" + (System.nanoTime() - l));  
  23.     }  
  24.   
  25.     public void catchException() {  
  26.         long l = System.nanoTime();  
  27.         for (int i = 0; i < testTimes; i++) {  
  28.             try {  
  29.                 throw new Exception();  
  30.             } catch (Exception e) {  
  31.             }  
  32.         }  
  33.         System.out.println("建立、抛出并接住异常对象:" + (System.nanoTime() - l));  
  34.     }  
  35.   
  36.     public static void main(String[] args) {  
  37.         ExceptionTest test = new ExceptionTest(10000);  
  38.         test.newObject();  
  39.         test.newException();  
  40.         test.catchException();  
  41.     }  
  42. }  
  43. //结果:
  44. 建立对象:575817  
  45. 建立异常对象:9589080  
  46. 建立、抛出并接住异常对象:47394475
复制代码
创建一个异常对象,是创建一个平凡Object耗时的约20倍(实际上差距会比这个数字更大一些,因为循环也占用了时间,追求准确的读者可以再测一下空循环的耗时然后在对比前减掉这部门),而抛出、接住一个异常对象,所耗费时间大约是创建异常对象的4倍。
关于作者

来自一线步伐员Seven的探索与实践,持续学习迭代中~
本文已收录于我的个人博客:https://www.seven97.top
公众号:seven97,接待关注~

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。




欢迎光临 ToB企服应用市场:ToB评测及商务社交产业平台 (https://dis.qidao123.com/) Powered by Discuz! X3.4