为什么Java编译器不检查未经检查的异常?

3
我的问题更多关于设计,Java编译器强制我们捕获已检查的异常(例如FileNotFoundException),但不强制我们捕获未检查的异常(例如NullPointerException)。我想了解原因,为什么编译器设计成这样?
解释Unchecked Exceptions — The Controversy表明这是为了提高可读性。
不捕获运行时异常难道不是一个巨大的代价吗?
编辑: 所谓的代价是指,在现场环境中而不是在编译期间获取运行时异常。如果此类异常在编译期间处理,就不可能发生错误泄漏。修复错误的成本随着检测到它们的阶段的延迟而增加。

7
“不捕获运行时异常难道不是一项巨大的代价吗?”-你指的是什么代价? 这种代价是什么? 理论上,拥有运行时异常意味着你的程序是错误的,你希望看到异常以找出程序错误。 - user2357112
2
FileNotFoundException取决于开发人员无法控制的因素。 如果您将jar文件交付给客户,他们可能会说:“算了吧,我认为我不需要那个文件”,但他们希望在屏幕上看到一个体面的错误消息,而不是系统崩溃,告诉他们“嗯,你确实需要那个文件。” NullPointerException,嗯,你真的如何准备好应对它呢?这并不是“外部”的问题,而是使用未实例化的实例。如果开发人员编写良好的代码,则不会发生这种情况,无论用户是否遵循要求。 - Stultuske
你的标题不正确。它应该是“为什么Java编译器不检查未经检查的异常?” - user207421
1
实际上,运行时异常对你有帮助。这就是为什么你不需要用try catch块包装任何数组访问、任何getter调用或算术除法操作,以及为什么运行时异常可以提高可读性。只需编写适当的测试,你就永远不会在运行时遇到运行时异常。 - dyrkin
6个回答

12
这样做的理由实际上包含在Java语言规范的第11.2节中。以下是相关摘录: 11.2. 异常的编译时检查

未经检查的异常类(§11.1.1)免于编译时检查。

在未经检查的异常类中,错误类被豁免,因为它们可能发生在程序的许多点,并且从中恢复是困难或不可能的。声明这些异常的程序将变得混乱,毫无意义。尽管如此,复杂的程序可能仍希望捕获并尝试从其中一些条件中恢复。

在未经检查的异常类中,运行时异常类被豁免,因为在Java编程语言的设计者看来,声明这些异常并不能有效地帮助确立程序的正确性。Java编程语言的许多操作和构造都可能在运行时导致异常。对于Java编译器可用的信息以及编译器执行的分析级别,通常不足以确定这样的运行时异常不会发生,即使这对程序员来说是显而易见的。要求声明这样的异常类只会给程序员带来麻烦。

例如,某些代码可能实现一个循环数据结构,该结构通过构造永远不涉及空引用;程序员可以确定NullPointerException不会发生,但Java编译器很难证明。建立数据结构的这些全局属性所需的定理证明技术超出了本规范的范围。

作为开发人员,我们可以在一定程度上控制代码是否会抛出未捕获的异常。例如,我们可以在尝试执行会导致异常的操作之前检查是否为null,或者编写代码以使该变量在第一次使用时就不可能为null,从而避免遇到NullPointerException。同样,我们可以通过实现合理性检查来确保输入绝对是数字,然后再将其解析为数字,以避免NumberFormatException等错误。

如果您编写的代码合理,应该很少(如果有的话)遇到RuntimeException(或其子类),因此要求您在每个可能引发异常的代码周围放置try-catch块将导致即使是简单的类也会有成百上千个小型try-catch块的可怕混乱,或者是一个捕获所有异常的巨大块,这两种情况都不是特别理想,并且会向您的代码添加大量不必要的膨胀。

被迫捕获所有未捕获的异常将使甚至像System.out.println()这样的“简单”操作变得更加冗长。

如果我们被迫捕获所有未经检查的异常(注意:这是最坏情况,异常只会传播而不在API内部处理),那么我们需要编写什么来将空行打印到控制台:

(译注:此处指代码)
System.out.println();
//         ^ Theoretically this could throw a NullPointerException

所以,我们必须考虑到这一点:
try {
    System.out.println();
} catch (NullPointerException e) {
    // In practice this won't happen, but we're forced to deal with
    // it all the same...
}

我们还没有完成。我们需要看看 out 是什么,以及 println() 如何工作,以确定是否还有其他需要处理的内容。out 实际上是一个 PrintStream 对象,那么我们可以从它的 println() 方法中得出什么信息呢?
这是 println():
public void println() {
    newLine();
}

这意味着我们现在需要看一下 newLine() 的作用...
private void newLine() {
    try {
        synchronized (this) {
            ensureOpen();
            textOut.newLine();
            textOut.flushBuffer();
            charOut.flushBuffer();
            if (autoFlush)
                out.flush();
        }
    }
    catch (InterruptedIOException x) {
        Thread.currentThread().interrupt();
    }
    catch (IOException x) {
        trouble = true;
    }
}

这里有更多关于NullPointerException的来源,但我们已经捕获了它。 interrupt()可能会(最终)抛出SecurityException,还可能导致InterruptedException,因此我们还需要处理这两个异常。

try {
    System.out.println();
} catch (NullPointerException e) {

} catch (SecurityException e) {

} catch (InterruptedException e) {

}

textOut.newLine() 最终会进入 Writer#write(String, int, int),该方法处理一个 char[],因此我们立即有了一个 ArrayIndexOutOfBoundsException 的来源。它还调用 String#getChars(int, int, char[], int),这个方法本身也可能抛出 StringIndexOutOfBoundsException。我们需要处理这个异常...

try {
    System.out.println();
} catch (NullPointerException e) {

} catch (SecurityException e) {

} catch (InterruptedException e) {

} catch (ArrayIndexOutOfBoundsException e) {

} catch (StringIndexOutOfBoundsException e) {

}

它还调用了BufferedWriter#write(char[], int, int),这可能会抛出IndexOutOfBoundsException异常...

try {
    System.out.println();
} catch (NullPointerException e) {

} catch (SecurityException e) {

} catch (InterruptedException e) {

} catch (ArrayIndexOutOfBoundsException e) {

} catch (StringIndexOutOfBoundsException e) {

} catch (IndexOutOfBoundsException e) {

}

我们在这个方法调用中现在有六个不同的运行时异常,这还不包括可能在途中出现的各种本地方法调用(其中有几个),也不包括任何Error子类。

如果我们真的被迫捕获所有异常,Java将因此变得更糟。


1
+1 是指引用官方规范中的实际解释,并展示如果做出不同决策会发生什么错误的示例。 - Brandon

3

设计涉及权衡取舍。设计一个能够处理所有情况的系统并不可行;我们已经足够困难地设计出能够处理相当常见情况的系统。

在我看来,检查异常和未检查异常之间的权衡是一种语言设计问题,在这种权衡被规范化,API 设计人员(无论是系统还是应用程序)可以确定是否强制编译器捕获给定异常。

  • 异常反映了某些重要的事情,使得调用本方法的调用方不允许只是“跳过它”并让程序崩溃吗?
  • 异常是否是应用程序员可能会忘记做的事情,因此应该至少写 catch (Exception e) {},以便人们能指出他们的错误?
  • 异常是否反映了应用程序员可能会认为是调用处理的情况,但实际上并非如此?

在我看来,Java 提供了一种很好的灵活性 -- API 设计人员拥有选择的权利。


1

运行时异常是由以下原因引起的:

  1. 程序员疏忽
  2. 错误数据

这两者都可以在代码本身中得到纠正,而且不应该发生。与预期会发生的已检查异常不同。

在下面的方法中,异常是由运行时错误的输入引起的。在调用函数之前应该纠正这个问题(错误的数据)。

Character.toChars(-2);

 public static char[] toChars(int codePoint) {
        if (isBmpCodePoint(codePoint)) {
            return new char[] { (char) codePoint };
        } else if (isValidCodePoint(codePoint)) {
            char[] result = new char[2];
            toSurrogates(codePoint, result, 0);
            return result;
        } else {
            throw new IllegalArgumentException();
        }
    }

1
因为如果它不是这样,Java将无法作为一种语言使用。您提到了最突出的例子:NullPointerException。每当访问实例字段或方法或访问数组时,都可能发生这种情况。这是所有Java代码行的很大比例。基本上,NullPointerException可以发生在任何地方,还有其他一些情况(例如ClassCastException、ArrayIndexOutOfBoundsException)。如果你被迫在每个地方都捕获它们,你甚至怎么能编写代码呢?而且你会在catch块中做什么?记录日志是不可能的,因为它需要一个方法调用,这可能会抛出NullPointerException,而您必须处理它...说实话,检查异常是一种失败的语言设计实验。没有其他语言采用它们,Java社区的大部分人都尽可能避免使用它们。

我认为“检查异常”的概念并不是一个坏主意,如果使用正确,它们可以非常好用。不幸的是,Sun/Oracle自己在很多地方都误用了它们(其中最明显的一个是SQLException)... - JonK

0
程序无法编译,因为存在未声明的异常,即FileNotFoundException。为了使程序能够正常运行而不崩溃,必须捕获或声明该异常被抛出。

-1

因为未经检查的异常不需要被捕获,而经过检查的异常需要被处理。因此,编译器“强制”你捕获经过检查的异常,并让你的未经检查的异常保持未被捕获状态。

更多解释请参见:Java中的已检查异常与未检查异常


3
楼主知道这件事情。他在询问为什么做出那个决定。 - Brandon
1
我们需要了解为什么未检查异常不需要被捕获。 - Jaikrat

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接