关于java:为什么捕获检查异常允许不抛出异常的代码?

Why is catching checked exceptions allowed for code that does not throw exceptions?

在Java中,抛出检查异常的方法(异常或它的子类型-IOExtRebug、中断ExtExcCEP等)必须声明抛出语句:

1
public abstract int read() throws IOException;

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

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

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

1
2
3
4
5
6
7
8
9
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不是一个检查过的异常,并允许重新执行它。事情甚至有点荒谬,这段代码不会编译:

1
2
3
4
5
6
7
8
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会发生什么潜在的错误?我们非常欣赏这些例子。


引用Java语言规范,{112.3:

It is a compile-time error if a catch clause can catch checked exception class E1 and it is not the case that the try block corresponding to the catch clause can throw a checked exception class that is a subclass or superclass of E1, unless E1 is Exception or a superclass of Exception.

我猜想这个规则早在Java 7之前就已经出现了,在那里没有多捕获。因此,如果您有一个可以抛出许多异常的try块,那么捕获所有内容的最简单方法就是捕获一个公共超类(在最坏的情况下,如果您还想捕获Error,则为ExceptionThrowable)。

请注意,您可能无法捕获与实际抛出的内容完全无关的异常类型-在您的示例中,捕获不是RuntimeExceptionThrowable的任何子类将是一个错误:

1
2
3
4
5
try {
    System.out.println("hello");
} catch (IOException e) {  // compilation error
    e.printStackTrace();
}


按op编辑:答案的主要部分是问题示例仅适用于异常类。通常,不允许在代码的随机位置捕获选中的异常。抱歉,如果我用这些例子把别人搞糊涂了。


Java 7引入了更具包容性的异常类型检查。

However, in Java SE 7, you can specify the exception types FirstException and SecondException in the throws clause in the rethrowException method declaration. The Java SE 7 compiler can determine that the exception thrown by the statement throw e must have come from the try block, and the only exceptions thrown by the try block can be FirstException and SecondException.

本文讨论的是一个try块,专门抛出FirstExceptionSecondException;虽然catch块抛出Exception,但方法只需声明它抛出FirstExceptionSecondException,而不是Exception

1
2
3
4
5
6
7
8
9
public void rethrowException(String exceptionName)
 throws FirstException, SecondException {
   try {
     // ...
   }
   catch (Exception e) {
     throw e;
   }
 }

这意味着编译器可以检测到在test中抛出的唯一可能的异常类型是Errors或RuntimeExceptions,两者都不需要捕获。当您使用throw e;时,即使静态类型是Exception时,它也可以告诉您不需要声明或重新捕获。

但是当你把它投射到Exception时,这就绕过了这个逻辑。现在,编译器将其视为需要捕获或声明的普通Exception

将此逻辑添加到编译器中的主要原因是,当重新调用捕获这些特定子类型的通用Exception时,程序员只允许在throws子句中指定特定的子类型。但是,在这种情况下,它允许您捕获一个通用的Exception,而不必在throws子句中声明任何异常,因为没有可以抛出的特定类型是检查异常的。


这里的问题是,选中/未选中的异常限制会影响代码允许抛出的内容,而不是允许捕获的内容。虽然您仍然可以捕获任何类型的Exception,但只有未选中的类型才允许您再次实际抛出。(这就是将未选中的异常强制转换为选中的异常会破坏代码的原因。)

Exception捕获未检查的异常是有效的,因为未检查的异常(a.k.a.RuntimeExceptions)是异常的一个子类,并且遵循标准的多态性规则;它不会将捕获的异常转换为Exception,就像在Object中存储String不会将String转换为EDOCX1一样。〔30〕。多态性意味着一个可以保存Object的变量可以保存来自Object的任何东西(例如String)。同样,由于Exception是所有异常类型的超类,因此,Exception类型的变量可以保存从Exception派生的任何类,而不必将对象转换为Exception类型。考虑一下:

1
2
3
4
5
import java.lang.*;
// ...
public String iReturnAString() { return"Consider this!"; }
// ...
Object o = iReturnAString();

尽管变量的类型是Object,但o仍然存储String,不是吗?同样,在您的代码中:

1
2
3
4
5
try {
    safeMethod();
} catch (Exception e) { // catching checked exception
    throw e; // so I can throw... a checked Exception?
}

实际上这意味着"捕获任何与类EDCOX1,4"兼容的任何东西(即EDCOX1,4和它派生的任何东西)。同样的逻辑也在其他语言中使用;例如,在C++中,捕获一个EDCOX1,45个也将捕获EDCOX1,46,EDCOX1,47,EDCOX1,48,任何适当定义的用户创建的异常,等等。n,因为它们都来自于std::exception

tl;dr:你没有捕捉到选中的异常,你捕捉到了任何异常。只有将异常转换为选中的异常类型时,该异常才会成为选中的异常。