Java 基础 - 异常机制详解(四)

简介: Java 基础 - 异常机制详解

不要使用异常控制程序的流程

不应该使用异常控制应用的执行流程,例如,本应该使用if语句进行条件判断的情况下,你却使用异常处理,这是非常不好的习惯,会严重影响应用的性能


不要在finally块中使用return

try块中的return语句执行成功后,并不马上返回,而是继续执行finally块中的语句,如果此处存在return语句,则在此直接返回,无情丢弃掉try块中的返回点。


如下是一个反例:

private int x = 0;
public int checkReturn() {
    try {
        // x等于1,此处不返回
        return ++x;
    } finally {
        // 返回的结果是2
        return ++x;
    }

深入理解异常

TIP

我们再深入理解下异常,看下底层实现。

JVM处理异常的机制?

提到JVM处理异常的机制,就需要提及Exception Table,以下称为异常表。我们暂且不急于介绍异常表,先看一个简单的 Java 处理异常的小例子。

public static void simpleTryCatch() {
   try {
       testNPE();
   } catch (Exception e) {
       e.printStackTrace();
   }
}

上面的代码是一个很简单的例子,用来捕获处理一个潜在的空指针异常。


当然如果只是看简简单单的代码,我们很难看出什么高深之处,更没有了今天文章要谈论的内容。


所以这里我们需要借助一把神兵利器,它就是javap,一个用来拆解class文件的工具,和javac一样由JDK提供。


然后我们使用javap来分析这段代码(需要先使用javac编译)

//javap -c Main
 public static void simpleTryCatch();
    Code:
       0: invokestatic  #3                  // Method testNPE:()V
       3: goto          11
       6: astore_0
       7: aload_0
       8: invokevirtual #5                  // Method java/lang/Exception.printStackTrace:()V
      11: return
    Exception table:
       from    to  target type
           0     3     6   Class java/lang/Exception

看到上面的代码,应该会有会心一笑,因为终于看到了Exception table,也就是我们要研究的异常表。


异常表中包含了一个或多个异常处理者(Exception Handler)的信息,这些信息包含如下 :


from 可能发生异常的起始点

to 可能发生异常的结束点

target 上述from和to之前发生异常后的异常处理者的位置

type 异常处理者处理的异常的类信息

那么异常表用在什么时候呢


答案是异常发生的时候,当一个异常发生时


1.JVM会在当前出现异常的方法中,查找异常表,是否有合适的处理者来处理

2.如果当前方法异常表不为空,并且异常符合处理者的from和to节点,并且type也匹配,则JVM调用位于target的调用者来处理。

3.如果上一条未找到合理的处理者,则继续查找异常表中的剩余条目

4.如果当前方法的异常表无法处理,则向上查找(弹栈处理)刚刚调用该方法的调用处,并重复上面的操作。

5.如果所有的栈帧被弹出,仍然没有处理,则抛给当前的Thread,Thread则会终止。

6.如果当前Thread为最后一个非守护线程,且未处理异常,则会导致JVM终止运行。

以上就是JVM处理异常的一些机制。


try catch -finally


除了简单的try-catch外,我们还常常和finally做结合使用。比如这样的代码

public static void simpleTryCatchFinally() {
   try {
       testNPE();
   } catch (Exception e) {
       e.printStackTrace();
   } finally {
       System.out.println("Finally");
   }
}

同样我们使用javap分析一下代码

public static void simpleTryCatchFinally();
    Code:
       0: invokestatic  #3                  // Method testNPE:()V
       3: getstatic     #6                  // Field java/lang/System.out:Ljava/io/PrintStream;
       6: ldc           #7                  // String Finally
       8: invokevirtual #8                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      11: goto          41
      14: astore_0
      15: aload_0
      16: invokevirtual #5                  // Method java/lang/Exception.printStackTrace:()V
      19: getstatic     #6                  // Field java/lang/System.out:Ljava/io/PrintStream;
      22: ldc           #7                  // String Finally
      24: invokevirtual #8                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      27: goto          41
      30: astore_1
      31: getstatic     #6                  // Field java/lang/System.out:Ljava/io/PrintStream;
      34: ldc           #7                  // String Finally
      36: invokevirtual #8                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      39: aload_1
      40: athrow
      41: return
    Exception table:
       from    to  target type
           0     3    14   Class java/lang/Exception
           0     3    30   any
          14    19    30   any

和之前有所不同,这次异常表中,有三条数据,而我们仅仅捕获了一个Exception, 异常表的后两个item的type为any; 上面的三条异常表item的意思为:


如果0到3之间,发生了Exception类型的异常,调用14位置的异常处理者。

如果0到3之间,无论发生什么异常,都调用30位置的处理者

如果14到19之间(即catch部分),不论发生什么异常,都调用30位置的处理者。

再次分析上面的Java代码,finally里面的部分已经被提取到了try部分和catch部分。我们再次调一下代码来看一下

public static void simpleTryCatchFinally();
    Code:
      //try 部分提取finally代码,如果没有异常发生,则执行输出finally操作,直至goto到41位置,执行返回操作。  
       0: invokestatic  #3                  // Method testNPE:()V
       3: getstatic     #6                  // Field java/lang/System.out:Ljava/io/PrintStream;
       6: ldc           #7                  // String Finally
       8: invokevirtual #8                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      11: goto          41
      //catch部分提取finally代码,如果没有异常发生,则执行输出finally操作,直至执行got到41位置,执行返回操作。
      14: astore_0
      15: aload_0
      16: invokevirtual #5                  // Method java/lang/Exception.printStackTrace:()V
      19: getstatic     #6                  // Field java/lang/System.out:Ljava/io/PrintStream;
      22: ldc           #7                  // String Finally
      24: invokevirtual #8                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      27: goto          41
      //finally部分的代码如果被调用,有可能是try部分,也有可能是catch部分发生异常。
      30: astore_1
      31: getstatic     #6                  // Field java/lang/System.out:Ljava/io/PrintStream;
      34: ldc           #7                  // String Finally
      36: invokevirtual #8                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      39: aload_1
      40: athrow     //如果异常没有被catch捕获,而是到了这里,执行完finally的语句后,仍然要把这个异常抛出去,传递给调用处。
      41: return

Catch先后顺序的问题

我们在代码中的catch的顺序决定了异常处理者在异常表的位置,所以,越是具体的异常要先处理,否则就会出现下面的问题

private static void misuseCatchException() {
   try {
       testNPE();
   } catch (Throwable t) {
       t.printStackTrace();
   } catch (Exception e) { //error occurs during compilings with tips Exception Java.lang.Exception has already benn caught.
       e.printStackTrace();
   }
}

这段代码会导致编译失败,因为先捕获Throwable后捕获Exception,会导致后面的catch永远无法被执行。


Return 和finally的问题


这算是我们扩展的一个相对比较极端的问题,就是类似这样的代码,既有return,又有finally,那么finally导致会不会执行

public static String tryCatchReturn() {
   try {
       testNPE();
       return  "OK";
   } catch (Exception e) {
       return "ERROR";
   } finally {
       System.out.println("tryCatchReturn");
   }
}

答案是finally会执行,那么还是使用上面的方法,我们来看一下为什么finally会执行。

public static java.lang.String tryCatchReturn();
    Code:
       0: invokestatic  #3                  // Method testNPE:()V
       3: ldc           #6                  // String OK
       5: astore_0
       6: getstatic     #7                  // Field java/lang/System.out:Ljava/io/PrintStream;
       9: ldc           #8                  // String tryCatchReturn
      11: invokevirtual #9                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      14: aload_0
      15: areturn       返回OK字符串,areturn意思为return a reference from a method
      16: astore_0
      17: ldc           #10                 // String ERROR
      19: astore_1
      20: getstatic     #7                  // Field java/lang/System.out:Ljava/io/PrintStream;
      23: ldc           #8                  // String tryCatchReturn
      25: invokevirtual #9                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      28: aload_1
      29: areturn  //返回ERROR字符串
      30: astore_2
      31: getstatic     #7                  // Field java/lang/System.out:Ljava/io/PrintStream;
      34: ldc           #8                  // String tryCatchReturn
      36: invokevirtual #9                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      39: aload_2
      40: athrow  如果catch有未处理的异常,抛出去。

异常是否耗时?为什么耗时?

说用异常慢,首先来看看异常慢在哪里?有多慢?下面的测试用例简单的测试了建立对象、建立异常对象、抛出并接住异常对象三者的耗时对比:

public class ExceptionTest {  
    private int testTimes;  
    public ExceptionTest(int testTimes) {  
        this.testTimes = testTimes;  
    }  
    public void newObject() {  
        long l = System.nanoTime();  
        for (int i = 0; i < testTimes; i++) {  
            new Object();  
        }  
        System.out.println("建立对象:" + (System.nanoTime() - l));  
    }  
    public void newException() {  
        long l = System.nanoTime();  
        for (int i = 0; i < testTimes; i++) {  
            new Exception();  
        }  
        System.out.println("建立异常对象:" + (System.nanoTime() - l));  
    }  
    public void catchException() {  
        long l = System.nanoTime();  
        for (int i = 0; i < testTimes; i++) {  
            try {  
                throw new Exception();  
            } catch (Exception e) {  
            }  
        }  
        System.out.println("建立、抛出并接住异常对象:" + (System.nanoTime() - l));  
    }  
    public static void main(String[] args) {  
        ExceptionTest test = new ExceptionTest(10000);  
        test.newObject();  
        test.newException();  
        test.catchException();  
    }  
}  

运行结果:

建立对象:575817  
建立异常对象:9589080  
建立、抛出并接住异常对象:47394475

建立一个异常对象,是建立一个普通Object耗时的约20倍(实际上差距会比这个数字更大一些,因为循环也占用了时间,追求精确的读者可以再测一下空循环的耗时然后在对比前减掉这部分),而抛出、接住一个异常对象,所花费时间大约是建立异常对象的4倍。


那占用时间的“大头”:抛出、接住异常,系统到底做了什么事情?请参考这篇文章:


目录
相关文章
|
2月前
|
Java
在 Java 中捕获和处理自定义异常的代码示例
本文提供了一个 Java 代码示例,展示了如何捕获和处理自定义异常。通过创建自定义异常类并使用 try-catch 语句,可以更灵活地处理程序中的错误情况。
80 1
|
2月前
|
Java API 调度
如何避免 Java 中的 TimeoutException 异常
在Java中,`TimeoutException`通常发生在执行操作超过预设时间时。要避免此异常,可以优化代码逻辑,减少不必要的等待;合理设置超时时间,确保其足够完成正常操作;使用异步处理或线程池管理任务,提高程序响应性。
99 12
|
2月前
|
监控 Java 应用服务中间件
高级java面试---spring.factories文件的解析源码API机制
【11月更文挑战第20天】Spring Boot是一个用于快速构建基于Spring框架的应用程序的开源框架。它通过自动配置、起步依赖和内嵌服务器等特性,极大地简化了Spring应用的开发和部署过程。本文将深入探讨Spring Boot的背景历史、业务场景、功能点以及底层原理,并通过Java代码手写模拟Spring Boot的启动过程,特别是spring.factories文件的解析源码API机制。
92 2
|
2月前
|
Java
在 Java 中,如何自定义`NumberFormatException`异常
在Java中,自定义`NumberFormatException`异常可以通过继承`IllegalArgumentException`类并重写其构造方法来实现。自定义异常类可以添加额外的错误信息或行为,以便更精确地处理特定的数字格式转换错误。
46 1
|
1月前
|
Java 程序员
深入理解Java异常处理机制
Java的异常处理是编程中的一块基石,它不仅保障了代码的健壮性,还提升了程序的可读性和可维护性。本文将深入浅出地探讨Java异常处理的核心概念、分类、处理策略以及最佳实践,旨在帮助读者建立正确的异常处理观念,提升编程效率和质量。
128 1
|
1月前
|
Java 开发者 UED
深入探索Java中的异常处理机制##
本文将带你深入了解Java语言中的异常处理机制,包括异常的分类、异常的捕获与处理、自定义异常的创建以及最佳实践。通过具体实例和代码演示,帮助你更好地理解和运用Java中的异常处理,提高程序的健壮性和可维护性。 ##
54 2
|
1月前
|
Java 开发者
Java中的异常处理机制深度剖析####
本文深入探讨了Java语言中异常处理的重要性、核心机制及其在实际编程中的应用策略,旨在帮助开发者更有效地编写健壮的代码。通过实例分析,揭示了try-catch-finally结构的最佳实践,以及如何利用自定义异常提升程序的可读性和维护性。此外,还简要介绍了Java 7引入的多异常捕获特性,为读者提供了一个全面而实用的异常处理指南。 ####
56 2
|
2月前
|
运维 Java 编译器
Java 异常处理:机制、策略与最佳实践
Java异常处理是确保程序稳定运行的关键。本文介绍Java异常处理的机制,包括异常类层次结构、try-catch-finally语句的使用,并探讨常见策略及最佳实践,帮助开发者有效管理错误和异常情况。
109 5
|
1月前
|
开发框架 安全 Java
Java 反射机制:动态编程的强大利器
Java反射机制允许程序在运行时检查类、接口、字段和方法的信息,并能操作对象。它提供了一种动态编程的方式,使得代码更加灵活,能够适应未知的或变化的需求,是开发框架和库的重要工具。
52 3
|
1月前
|
Java 程序员 UED
深入理解Java中的异常处理机制
本文旨在揭示Java异常处理的奥秘,从基础概念到高级应用,逐步引导读者掌握如何优雅地管理程序中的错误。我们将探讨异常类型、捕获流程,以及如何在代码中有效利用try-catch语句。通过实例分析,我们将展示异常处理在提升代码质量方面的关键作用。
46 3