为什么允许捕获不会抛出异常的代码中的已检查异常?

35
在Java中,抛出已检查的异常的方法(Exception或其子类型 - IOException,InterruptedException等)必须声明throws语句:
public abstract int read() throws IOException;

没有声明 throws 语句的方法不能抛出已检查的异常。

public int read() { // does not compile
    throw new IOException();
}
// Error: unreported exception java.io.IOException; must be caught or declared to be thrown

但在Java中,在安全方法中捕获已检查的异常仍然是合法的:

public void safeMethod() { System.out.println("I'm safe"); }

public void test() { // method guarantees not to throw checked exceptions
    try {
        safeMethod();
    } catch (Exception e) { // catching checked exception java.lang.Exception
        throw e; // so I can throw... a checked Exception?
    }
}

实际上不是这样的。有点好笑:编译器知道 e 不是一个受检异常,因此允许重新抛出它。事情甚至有点荒谬,这段代码无法编译:
public void test() { // guarantees not to throw checked exceptions
    try {
        safeMethod();
    } catch (Exception e) {        
        throw (Exception) e; // seriously?
    }
}
// Error: unreported exception java.lang.Exception; must be caught or declared to be thrown

第一个片段是一个问题的动机。
编译器知道受检异常不能在安全方法中抛出 - 那么它是否应该允许仅捕获未检查的异常?

回到主要问题 - 实现这种方式捕获已检查异常的原因是什么?这只是设计上的缺陷还是我忽略了一些重要因素 - 也许是向后不兼容性?如果在此情况下仅允许捕获 RuntimeException ,可能会出现什么问题?举例说明将更有助于理解。


1
关于主要问题:这并不是设计上的缺陷,RuntimeException是Exception的子类,因此捕获Exception也包括未经检查的异常。 话虽如此,没有理由这样做,甚至可能会让阅读代码的人感到困惑,因为他们可能认为safeMethod()可能会抛出异常。 我认为在这里只捕获RuntimeException是更好的选择。 - MartinS
1
在 throw 语句的相关部分中,详细说明了该语句的使用。 - Makoto
@bayou.io这与为什么要使用泛型而不是原始类型相同吗?答案是更强的编译器检查。编译器可以告诉我们Exception不能被抛出,只能是RuntimeException,那么为什么要捕获它呢? - AdamSkywalker
1
@AdamSkywalker,我们知道原始类型引起的许多问题。捕获更宽的类型会导致什么问题?这就是为什么你的隐喻不成立的原因。按照你的论点,final Object ob = "foo";也应该导致编译器错误,因为我们在编译时知道ob的运行时类型将是String。 - biziclop
1
由于safeMethod()是安全的,这意味着捕获的Exception e必须是RuntimeException。如果保持不变(如第一个片段中所示),则一切都很好。但是,在第二个片段中显式转换为Exception时,您使编译器忘记了它所知道的,并相信它可能是任何Exception,这当然是不可以的。 - Erick G. Hagstrom
显示剩余2条评论
3个回答

19
引用自Java语言规范,§11.2.3

如果一个catch子句可以捕获已检查异常类E1,并且对应于该catch子句的try块不能抛出E1的超类或子类的已检查异常类,除非E1是Exception或Exception的超类,则会在编译时出现错误。

我猜这个规则早在Java 7之前就出现了,当时还不存在多重捕获。因此,如果您有一个可能抛出多种异常的try块,最简单的方法是捕获一个共同的超类(在最坏的情况下,是Exception或者Throwable,如果您想要捕获Error)。
请注意,您不可以捕获与实际抛出的异常完全无关的异常类型 - 在您的示例中,捕获任何非RuntimeException子类的Throwable都将导致错误。
try {
    System.out.println("hello");
} catch (IOException e) {  // compilation error
    e.printStackTrace();
}


编辑者注:答案的主要部分是问题示例仅适用于异常类。通常情况下,不允许在代码的随意位置捕获已检查异常。如果我使用这些示例使某人感到困惑,我很抱歉。


4
确切地说,在使用 try-multi-catch 规则之前,这条规则就已经存在了很久,可能是从1.0版本开始(但肯定是从1.2版本开始)。 - biziclop
1
@AdamSkywalker 这个规则很简单:如果可能抛出异常 E,那么任何 E 的超类都可以被捕获。为什么这个规则在历史上是必要的已经在答案中得到了解释。你所建议的问题是,规则会变得更加混乱和复杂,以解决一个根本不存在的问题。 - biziclop
1
AdamSkywalker:是的,但语言设计者/编译器作者也需要防止事情变得不必要地更加复杂。正如@biziclop所建议的那样,您提出的修改方案可以解决一个非常微小的问题,但会使规则及其实现变得更加复杂。 - Aasmund Eldhuset
1
@TimBender:我想集中讨论异常,但我同意我的原始措辞让人觉得异常是唯一可能抛出的东西。我已经编辑了我的回答。 - Aasmund Eldhuset
1
@AdamSkywalker: 另外,由于任何东西都可能抛出 Error,所以捕获 Throwable 必须始终被允许,因此规则看起来会更加复杂:“您可以捕获所有可能被抛出的异常类型的任何子类、同级和包括它们的最低公共祖先,此外还可以捕获 Throwable。”与“您可以捕获可能被抛出的任何子类或超类。”相比。 - Aasmund Eldhuset
显示剩余2条评论

11

Java 7推出了更加包容的异常类型检查

然而,在Java SE 7中,你可以在rethrowException方法声明的throws子句中指定异常类型FirstException和SecondException。Java SE 7编译器可以确定语句throw e所抛出的异常必须来自try块,而try块所抛出的唯一异常可以是FirstException和SecondException。

这段话讲述了一个特定抛出FirstExceptionSecondExceptiontry块;即使catch块抛出Exception,该方法只需要声明它抛出FirstExceptionSecondException,而不是Exception

public void rethrowException(String exceptionName)
 throws FirstException, SecondException {
   try {
     // ...
   }
   catch (Exception e) {
     throw e;
   }
 }
这意味着编译器可以检测到test中可能抛出的唯一异常类型是ErrorRuntimeException,这两个都不需要被捕获。当你throw e;时,即使静态类型是Exception,编译器也能知道它不需要被声明或重新捕获。
但是,当你将其强制转换为Exception时,这就绕过了那个逻辑。现在编译器将其视为需要被捕获或声明的普通异常。
编译器添加此逻辑的主要原因是允许程序员在重新抛出一个一般的Exception并捕获特定子类型时仅指定特定子类型。然而,在这种情况下,它允许你捕获一个一般的Exception并且不必在throws子句中声明任何异常,因为没有检查异常可能抛出的具体类型。

我理解为什么第一段代码可以编译而第二段不能。我认为我应该用粗体字包裹主要问题。 - AdamSkywalker
我并不认为这完全解释了语义层面上发生的事情。是的,它描述了行为,但我并不确定这种方式是真正的答案。 - Makoto
4
始终可以捕获可能抛出的所有异常的超类,例如想象一下文件操作方法,其中包装了可能抛出FileNotFoundException和IIOException的代码。在try-multi-catch之前,处理两者的唯一方法是要么有两个完全相同的catch子句,要么捕获其超类,例如IOException或Exception。这两种解决方案都有缺点,但第二种比较好一些。 - biziclop
可以抛出类型为 Exception 的异常。有很多糟糕的代码,其中的方法只是声明 throws Exception。异常本身可以被构造并抛出,而无需进一步对其进行子类化。同样地,所有可抛出的东西的超类型 Throwable 也可以这样做。 - Tim Bender
1
@TimBender 还有一些相当合理的情况,比如在反射中。 - biziclop
显示剩余2条评论

7
问题在于,已检查/未检查的异常限制影响您的代码允许抛出的内容,而不是允许捕获的内容。虽然您仍然可以捕获任何类型的Exception,但您实际上只能再次抛出未经检查的异常。(这就是为什么将未经检查的异常强制转换为已检查的异常会破坏您的代码的原因。)
使用Exception捕获未经检查的异常是有效的,因为未经检查的异常(也称为RuntimeException)是Exception的子类,并遵循标准多态规则;它不会将捕获的异常转换为Exception,就像将String存储在Object中不会将String变成Object一样。多态意味着一个可以容纳Object的变量可以容纳任何派生自Object的内容(例如String)。同样,由于Exception是所有异常类型的超类,类型为Exception的变量可以容纳任何派生自Exception的类,而不会将对象转换为Exception。请考虑以下内容:
import java.lang.*;
// ...
public String iReturnAString() { return "Consider this!"; }
// ...
Object o = iReturnAString();

尽管变量类型是Objecto仍然存储了一个String,是吗?同样,在您的代码中:
try {
    safeMethod();
} catch (Exception e) { // catching checked exception
    throw e; // so I can throw... a checked Exception?
}

这意味着实际上“捕获与类 Exception 兼容的任何内容(即 Exception 及其派生类)”。其他语言中也使用类似的逻辑,例如,在C ++中,捕获 std :: exception 也会捕获 std :: runtime_error std :: logic_error std :: bad_alloc ,任何正确定义的用户创建异常等,因为它们都是从 std :: exception 派生而来。

简而言之:您不是在捕获“已检查”的异常,而是在捕获“任何”异常。只有将其转换为已检查的异常类型,异常才成为已检查异常。


4
请不要轻视那些试图帮助你的人。多态性的论点是完全有效的,因为 catch (Exception ex) 实际上就像 Exception ex = ... 一样。 - biziclop

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