你知道吗,Java中的受查和非受查异常,其实并不存在区别……
本文最后更新于 231 天前,其中的信息可能已经有所发展或是发生改变。
由 ChatGPT 生成的文章摘要
博主在文章中提到Java中的受查异常和非受查异常之间的区别在JVM的世界中实际上并不存在。传统理念认为继承自java.lang.RuntimeException的异常是非受查异常,其他异常则是受查异常。然而,通过比较JVM字节码层面的代码表示,发现无论是受查异常还是非受查异常,在JVM字节码中并没有实质差别。此外,文章也讨论了Kotlin语言对于受查异常的规避,通过Kotlin编写的类似Java代码可以正常运行且不需要显式捕获异常。最后,文章提到Java中的受查异常机制存在争议,而Kotlin作为一种新的JVM语言,避免了这一问题。

你知道吗,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 does not have checked exceptions. There are many reasons for this, but we will provide a simple example that illustrates why it is the case.
Kotlin 没有受查异常。造成这种情况的原因有很多,但我们将提供一个简单的示例来说明为什么会出现这种情况。

The following is an example interface from the JDK implemented by the StringBuilder class:
以下是 JDK 中由 StringBuilder 类实现的示例接口:

Appendable append(CharSequence csq) throws IOException;

This signature says that every time I append a string to something (a StringBuilder, some kind of a log, a console, etc.), I have to catch the IOExceptions. Why? Because the implementation might be performing IO operations (Writer also implements Appendable). The result is code like this all over the place:
这个签名表明,每次我将字符串附加到某些东西( StringBuilder 、某种日志、控制台等)时,我都必须捕获 IOExceptions 。为什么?因为该实现可能正在执行 IO 操作( Writer 也实现 Appendable )。结果到处都是这样的代码:

try {
 log.append(message)
} catch (IOException e) {
 // Must be safe
}

And that's not good. Just take a look at Effective Java, 3rd Edition, Item 77: Don't ignore exceptions.
这很不好。只需看一下《Effective Java》,第 3 版,第 77 条:不要忽略异常。

Bruce Eckel says this about checked exceptions:
Bruce Eckel 对于受查异常是这样说的:

Examination of small programs leads to the conclusion that requiring exception specifications could both enhance developer productivity and enhance code quality, but experience with large software projects suggests a different result – decreased productivity and little or no increase in code quality.
对小型程序的检查得出的结论是,要求异常规范既可以提高开发人员的生产力,又可以提高代码质量,但大型软件项目的经验表明了不同的结果——生产力下降,代码质量几乎没有提高。

......

—— https://kotlinlang.org/docs/exceptions.html#checked-exceptions

那么对于和上述代码类似的 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 属性表。万里长城今犹在,不见当年秦始皇。

扫码关注 HikariLan's Blog 微信公众号,及时获取最新博文!


微信公众号图片
暂无评论

发送评论 编辑评论

|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇