你知道吗,Java中的受查和非受查异常,其实并不存在区别..

相信写过 Java 的人都会知道,在 Java 的异常系统中,存在“受查(checked)”异常和“非受查(unchecked)”两座大山,两者虽然均为异常,但是却有着微妙的区别。但是你知道吗,实际上在 JVM 的世界里,这种区别根本不存在......

“受查”和“非受查”

为什么有时候调用某些方法的时候需要强制 try-catch 它们,亦或者在调用方法上加入 throws 关键字声明抛出,而有的方法虽然会抛出异常,但是并不会要求你这么做...... 如果有一位 Java 新手带着这样的疑惑问你,你一定会轻车熟路的告诉他:所有继承自 java.lang.RuntimeException 的异常,他们都是非受查异常,这些异常允许你不必强制在方法体上声明他们,亦或者强制通过 try-catch 捕获;而除此之外的异常,则都是受查异常,你必须按照上述的方法声明和捕获他们。

举个例子:以下代码是无法正常编译的:

import java.io.IOException;

public class Main {

    public static void main(String[] args){
        throw new IOException("Goodbye, World!");
    }

}

因为 java.io.IOException 没有继承自 java.lang.RuntimeException,因此是一个非受查异常,而我们并没有通过 try-catch 捕获异常或是在调用函数上声明抛出该异常。因此我们会得到如下编译错误:

Main.java:6: error: unreported exception IOException; must be caught or declared to be thrown
        throw new IOException("Goodbye, World!");
        ^

改进措施也很简单,在 main 方法上声明抛出异常即可正常编译:

import java.io.IOException;

public class Main {

    public static void main(String[] args) throws IOException {
        throw new IOException("Goodbye, World!");
    }

}

亦或者,我们也可以通过 try-catch 来捕获这个异常:

import java.io.IOException;

public class Main {

    public static void main(String[] args) {
        try {
            throw new IOException("Goodbye, World!");
        } catch (IOException e){
            throw new RuntimeException("Caught!");
        }
    }

}

我们需要更深入点

而如果你是一个善于提出问题的人,你可能会接着问下去:既然 Java 代码最终会编译为 JVM 字节码,那么在 JVM 字节码层面,这些代码是如何表示的呢?

通过 javap 实用工具,我们得以有机会一窥上述代码的真面孔:

public class Main
  minor version: 0
  major version: 61
  flags: (0x0021) ACC_PUBLIC, ACC_SUPER
  this_class: #14                         // Main
  super_class: #2                         // java/lang/Object
  interfaces: 0, fields: 0, methods: 2, attributes: 1
Constant pool:
   #1 = Methodref          #2.#3          // java/lang/Object."<init>":()V
   #2 = Class              #4             // java/lang/Object
   #3 = NameAndType        #5:#6          // "<init>":()V
   #4 = Utf8               java/lang/Object
   #5 = Utf8               <init>
   #6 = Utf8               ()V
   #7 = Class              #8             // java/io/IOException
   #8 = Utf8               java/io/IOException
   #9 = String             #10            // Goodbye, World!
  #10 = Utf8               Goodbye, World!
  #11 = Methodref          #7.#12         // java/io/IOException."<init>":(Ljava/lang/String;)V
  #12 = NameAndType        #5:#13         // "<init>":(Ljava/lang/String;)V
  #13 = Utf8               (Ljava/lang/String;)V
  #14 = Class              #15            // Main
  #15 = Utf8               Main
  #16 = Utf8               Code
  #17 = Utf8               LineNumberTable
  #18 = Utf8               main
  #19 = Utf8               ([Ljava/lang/String;)V
  #20 = Utf8               Exceptions
  #21 = Utf8               SourceFile
  #22 = Utf8               Main.java
{
  public Main();
    descriptor: ()V
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 3: 0

  public static void main(java.lang.String[]) throws java.io.IOException;
    descriptor: ([Ljava/lang/String;)V
    flags: (0x0009) ACC_PUBLIC, ACC_STATIC
    Code:
      stack=3, locals=1, args_size=1
         0: new           #7                  // class java/io/IOException
         3: dup
         4: ldc           #9                  // String Goodbye, World!
         6: invokespecial #11                 // Method java/io/IOException."<init>":(Ljava/lang/String;)V
         9: athrow
      LineNumberTable:
        line 6: 0
    Exceptions:
      throws java.io.IOException
}

眼尖的你可能已经注意到最下面两行已经展示出了我们想要的东西:我们在方法声明中填写的异常抛出声明,会作为 JVM 字节码方法表中的 Exception 属性表的一部分提供给 JVM 虚拟机。

而当我们通过 try-catch 来显式捕获异常的时候,它看起来是这样的:

public class Main
  minor version: 0
  major version: 61
  flags: (0x0021) ACC_PUBLIC, ACC_SUPER
  this_class: #19                         // Main
  super_class: #2                         // java/lang/Object
  interfaces: 0, fields: 0, methods: 2, attributes: 1
Constant pool:
   #1 = Methodref          #2.#3          // java/lang/Object."<init>":()V
   #2 = Class              #4             // java/lang/Object
   #3 = NameAndType        #5:#6          // "<init>":()V
   #4 = Utf8               java/lang/Object
   #5 = Utf8               <init>
   #6 = Utf8               ()V
   #7 = Class              #8             // java/io/IOException
   #8 = Utf8               java/io/IOException
   #9 = String             #10            // Goodbye, World!
  #10 = Utf8               Goodbye, World!
  #11 = Methodref          #7.#12         // java/io/IOException."<init>":(Ljava/lang/String;)V
  #12 = NameAndType        #5:#13         // "<init>":(Ljava/lang/String;)V
  #13 = Utf8               (Ljava/lang/String;)V
  #14 = Class              #15            // java/lang/RuntimeException
  #15 = Utf8               java/lang/RuntimeException
  #16 = String             #17            // Caught!
  #17 = Utf8               Caught!
  #18 = Methodref          #14.#12        // java/lang/RuntimeException."<init>":(Ljava/lang/String;)V
  #19 = Class              #20            // Main
  #20 = Utf8               Main
  #21 = Utf8               Code
  #22 = Utf8               LineNumberTable
  #23 = Utf8               main
  #24 = Utf8               ([Ljava/lang/String;)V
  #25 = Utf8               StackMapTable
  #26 = Utf8               SourceFile
  #27 = Utf8               Main.java
{
  public Main();
    descriptor: ()V
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 3: 0

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: (0x0009) ACC_PUBLIC, ACC_STATIC
    Code:
      stack=3, locals=2, args_size=1
         0: new           #7                  // class java/io/IOException
         3: dup
         4: ldc           #9                  // String Goodbye, World!
         6: invokespecial #11                 // Method java/io/IOException."<init>":(Ljava/lang/String;)V
         9: athrow
        10: astore_1
        11: new           #14                 // class java/lang/RuntimeException
        14: dup
        15: ldc           #16                 // String Caught!
        17: invokespecial #18                 // Method java/lang/RuntimeException."<init>":(Ljava/lang/String;)V
        20: athrow
      Exception table:
         from    to  target type
             0    10    10   Class java/io/IOException
      LineNumberTable:
        line 7: 0
        line 8: 10
        line 9: 11
      StackMapTable: number_of_entries = 1
        frame_type = 74 /* same_locals_1_stack_item */
          stack = [ class java/io/IOException ]
}

try-catch 会被转换为 JVM 字节码的异常表(Exception table),异常表会负责捕获指定范围内(from 和 to)的指定类型异常(type),当异常抛出时,将代码跳转到指定的 JVM 代码行中(target)。

看到这里你可能就会开始提问:那么受查异常和非受查异常的差别呢,如何体现在 JVM 字节码里呢?

而答案是:完全没有区别。

编译器诡计:所见不一定所得

其实 Java 中并不缺乏这种“编译器诡计”的例子,从泛型到自动拆装箱,从字符串连接再到 lambda 表达式...... Java 的语言设计者赋予 Java 编译器巨大的魔力,在不变动中间表示代码(这里是 JVM 字节码)的情况下提供更多的语法特性或者语义限制。而受查异常和非受查异常显然就是其中的一部分 —— 在 JVM 字节码的层面,它们不能说是一模一样,只能说是毫无区别。

Kotlin: 规则破坏者

其实 Java 的受查异常是一个饱受诟病的语法特性,就和 Java 的泛型一样远近闻名:这些异常声明可能会随着调用链的增加越来越长,而有时也许你根本不想捕获这些异常,你只想简单的抛出他们。Java 社区中著名的 Lombok 项目甚至专门提供了一个 @SneakyThrows 注解来替你生成这些冗长的模板代码。那么是否有一个 JVM 语言抛弃了这个设定?答案是肯定的,那就是大名鼎鼎的 Kotlin。

那么对于和上述代码类似的 Kotlin 代码:

import java.io.IOException;

fun main(){
     throw IOException("Goodbye, World!");
}

可以正常通过编译并运行。那么 Kotlin 是做了什么魔法呢?依然用 javap 来看看:

public final class MainKt
  minor version: 0
  major version: 52
  flags: (0x0031) ACC_PUBLIC, ACC_FINAL, ACC_SUPER
  this_class: #2                          // MainKt
  super_class: #4                         // java/lang/Object
  interfaces: 0, fields: 0, methods: 2, attributes: 2
Constant pool:
   #1 = Utf8               MainKt
   #2 = Class              #1             // MainKt
   #3 = Utf8               java/lang/Object
   #4 = Class              #3             // java/lang/Object
   #5 = Utf8               main
   #6 = Utf8               ()V
   #7 = Utf8               java/io/IOException
   #8 = Class              #7             // java/io/IOException
   #9 = Utf8               Goodbye, World!
  #10 = String             #9             // Goodbye, World!
  #11 = Utf8               <init>
  #12 = Utf8               (Ljava/lang/String;)V
  #13 = NameAndType        #11:#12        // "<init>":(Ljava/lang/String;)V
  #14 = Methodref          #8.#13         // java/io/IOException."<init>":(Ljava/lang/String;)V
  #15 = Utf8               ([Ljava/lang/String;)V
  #16 = NameAndType        #5:#6          // main:()V
  #17 = Methodref          #2.#16         // MainKt.main:()V
  #18 = Utf8               args
  #19 = Utf8               [Ljava/lang/String;
  #20 = Utf8               Lkotlin/Metadata;
  #21 = Utf8               mv
  #22 = Integer            1
  #23 = Integer            9
  #24 = Integer            0
  #25 = Utf8               k
  #26 = Integer            2
  #27 = Utf8               xi
  #28 = Integer            48
  #29 = Utf8               d1
  #30 = Utf8               \u0000\u0006\n\u0000\n\u0002\u0010\u0002\u001a\u0006\u0010\u0000\u001a\u00020\u0001
  #31 = Utf8               d2
  #32 = Utf8
  #33 = Utf8               Main.kt
  #34 = Utf8               Code
  #35 = Utf8               LineNumberTable
  #36 = Utf8               LocalVariableTable
  #37 = Utf8               SourceFile
  #38 = Utf8               RuntimeVisibleAnnotations
{
  public static final void main();
    descriptor: ()V
    flags: (0x0019) ACC_PUBLIC, ACC_STATIC, ACC_FINAL
    Code:
      stack=3, locals=0, args_size=0
         0: new           #8                  // class java/io/IOException
         3: dup
         4: ldc           #10                 // String Goodbye, World!
         6: invokespecial #14                 // Method java/io/IOException."<init>":(Ljava/lang/String;)V
         9: athrow
      LineNumberTable:
        line 4: 0

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: (0x1009) ACC_PUBLIC, ACC_STATIC, ACC_SYNTHETIC
    Code:
      stack=0, locals=1, args_size=1
         0: invokestatic  #17                 // Method main:()V
         3: return
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       4     0  args   [Ljava/lang/String;
}

作为受查异常的 IOException 依然通过 athrow 指令照常抛出,但是却没有任何的处理措施 —— 无论是异常表还是 Exception 属性表。万里长城今犹在,不见当年秦始皇。

全部评论

相关推荐

Bug压路:老哥看得出来你是想多展示一些项目,但好像一般最多两个就够了😂页数一般一页,多的也就2页;这些项目应该是比较同质化的,和评论区其他大佬一样,我也觉得应该展示一些最拿手的(质量>数量)😁😁😁专业技能部分也可以稍微精简一些
点赞 评论 收藏
分享
2 1 评论
分享
牛客网
牛客企业服务