你知道吗,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 属性表。万里长城今犹在,不见当年秦始皇。

全部评论

相关推荐

base北京,已入职面试体验:很好,面试官很温柔,讲话也很平和,慢慢的。面完一面个人觉得自己发挥的不太好(太紧张了),但是居然过了(入职后补充:一面面试官是我的mentor,确实是个很nice的人)一面:1.&nbsp;自我介绍。2.&nbsp;问实习经历。&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;1.&nbsp;之前是有两段实习经历是吧?&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;2.&nbsp;介绍一下在游戏测试的实习经历。(负责的模块,跟版细节,是否主动考虑过提效)3.&nbsp;Java抽象类和接口之间的区别和联系。4.&nbsp;解释一下final限制类扩展的底层原理。5.&nbsp;介绍一下反射。为什么要用反射?什么场景会用反射?通过反射最后获取到的是什么?6.&nbsp;如果类之间有继承关系,所有对象都可以通过反射一直拿到同一个类吗?为什么?知道底层原理吗?7.&nbsp;多线程是怎么实现的,你最喜欢哪一种创建线程的方法。8.&nbsp;拷打项目。&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;1.&nbsp;介绍一下项目。&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;2.&nbsp;项目里用到了AOP吗?&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;3.&nbsp;项目里的日志是如何实现的?&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;4.&nbsp;mybatis在项目里如何运用的?&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;5.&nbsp;mybatis标签在底层是如何转化为sql去执行的。&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;6.&nbsp;项目里的session如何实现的,session里存储了哪些信息?9.&nbsp;Linux经常用吧,查找文件的命令知道吗?10.&nbsp;vim中的编辑方法,比如如何删除行之类的。11.&nbsp;代码题3个&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;1.&nbsp;多线程循环打印ABBC&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;2.&nbsp;合并数组&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;3.&nbsp;sql查询12.&nbsp;还有哪些擅长的东西想聊。(回答了网络)13.&nbsp;常用的网络协议有哪些。14.&nbsp;你说的这些协议项目里都用过吗?15.&nbsp;smtp和http使用上的差别是什么?16.&nbsp;为什么不读研。17.&nbsp;为什么投测试。18.&nbsp;学校方面情况。19.&nbsp;反问————————————面试完第二天中午电话通知一面通过。 #面经# #蔚来# #实习#&nbsp;&nbsp;
投递蔚来等公司7个岗位
点赞 评论 收藏
转发
2 1 评论
分享
牛客网
牛客企业服务