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