为啥不抛出异常的代码允许捕获已检查的异常?

Posted

技术标签:

【中文标题】为啥不抛出异常的代码允许捕获已检查的异常?【英文标题】:Why is catching checked exceptions allowed for code that does not throw exceptions?为什么不抛出异常的代码允许捕获已检查的异常? 【发布时间】:2016-05-13 01:34:37 【问题描述】:

在 Java 中,抛出 checked 异常(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

第一个 sn-p 是提出问题的动机。

编译器知道不能在安全方法中抛出已检查的异常 - 所以也许它应该只允许捕获未检查的异常?


回到主要问题 - 是否有任何理由以这种方式实现捕获检查的异常?这只是设计中的一个缺陷,还是我错过了一些重要因素——可能是向后不兼容?如果在这种情况下只允许捕获RuntimeException,可能会出现什么问题?非常感谢示例。

【问题讨论】:

关于主要问题:这不是设计本身的缺陷,RuntimeExceptions是Exception的子类,因此捕获Exception也包括未经检查的异常。话虽如此,没有理由这样做,它甚至可能会使阅读代码的人感到困惑,因为他们可能认为 safeMethod() 可能会抛出异常。我认为在这里捕获 RuntimeException 是一个更好的选择。 Relevant section of the JLS on the throw statement. 你甚至可以捕捉到Throwable。捕捉更一般的类型有什么问题? @AdamSkywalker 我们知道原始类型会导致许多问题。捕获更广泛的类型会导致什么问题?这就是你的比喻失效的原因。根据您的论点,final Object ob = "foo"; 也应该导致编译器错误,因为我们在编译时知道ob 的运行时类型将是String 既然safeMethod() 是安全的,这意味着被捕获的Exception e 必须是 RuntimeException。如果它保持原样(如在第一个 sn-p 中)一切都很好。但是,当您在第二个 sn-p 中显式转换为 Exception 时,您会使编译器忘记它所知道的并相信它可能是任何 Exception,这当然是不行的。 【参考方案1】:

引用Java Language Specification, §11.2.3:

如果catch 子句可以捕获已检查的异常类E1,并且与catch 子句对应的try 块不能抛出作为E1 的子类或超类的已检查异常类,则这是编译时错误,除非 E1 是 Exception 或 Exception 的超类。

我猜这个规则早在 Java 7 之前就已经出现了,在 Java 7 中不存在多重捕获。因此,如果你有一个try 块可能会引发大量异常,那么捕获所有内容的最简单方法是捕获一个公共超类(在最坏的情况下,ExceptionThrowable,如果你想捕获Errors 也是如此)。

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

try 
    System.out.println("hello");
 catch (IOException e)   // compilation error
    e.printStackTrace();


由 OP 编辑​​: 答案的主要部分是问题示例仅适用于 Exception 类这一事实。通常不允许在代码的随机位置捕获已检查的异常。抱歉,如果我使用这些示例使某人感到困惑。

【讨论】:

没错,这条规则早在 try-multi-catch 之前就存在了,可能从 1.0 开始(但肯定是从 1.2 开始)。 @AdamSkywalker 规则很简单:如果可以抛出异常E,则可以捕获E 的任何超类。为什么该规则在历史上是必要的,已在答案中明确说明。您所建议的问题在于,要解决一个甚至不存在的问题,规则会变得更加混乱和复杂。 AdamSkywalker:是的,但是语言设计者/编译器作者也需要防止事情变得过于复杂。正如@biziclop 所建议的那样,您提出的修改将解决一个非常 小问题,但会使规则及其实施复杂化。 @TimBender:我想专注于异常,但我同意我最初的措辞听起来好像异常是唯一可以抛出的东西。我已经编辑了我的答案。 @AdamSkywalker:此外,由于任何事情都可能抛出Error,因此必须始终允许捕获Throwable,因此规则看起来更加复杂:“您可以捕获任何子类,并且直到并包括所有可能抛出的异常类型的最低共同祖先,此外,您可以捕获Throwable。” - vs. “你可以捕获任何可能被抛出的子类或超类。”【参考方案2】:

Java 7 引入了more inclusive exception type checking。

但是,在 Java SE 7 中,您可以在 rethrowException 方法声明的 throws 子句中指定异常类型 FirstException 和 SecondException。 Java SE 7 编译器可以判断语句 throw e 抛出的异常一定来自 try 块,try 块抛出的异常只能是 FirstException 和 SecondException。

这段话是在讨论一个try 块,它专门抛出FirstExceptionSecondException;即使catch块抛出Exception,该方法只需要声明它抛出FirstExceptionSecondException,而不是Exception

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 子句中声明 any 异常,因为没有可以抛出的特定类型是检查异常。

【讨论】:

我明白为什么第一个代码可以编译而第二个没有。我想我应该用粗体来包装主要问题。 主要问题在标题中 - 如果我们知道它不能被抛出,为什么还要允许捕获它 我不相信这在语义层面上完全解释了正在发生的事情。是的,它描述了行为,但我不相信这种事情是真正的答案。 @AdamSkywalker 总是可以捕获所有可能抛出的异常的超类,例如,想象一个文件操作方法,它包装了可以抛出 FileNotFoundExceptionIIOException 的代码.在try-multi-catch 之前,处理两者的唯一方法是拥有两个相同的catch 子句,或者捕获它的超类,例如IOException。或Exception。两种解决方案在某种程度上都不好,但第二种解决方案稍微不那么糟糕。 @TimBender 还有一些相当合理的案例,比如反思。【参考方案3】:

这里的问题是检查/未检查异常限制会影响您的代码允许抛出的内容,而不是允许捕获的内容。虽然您仍然可以捕捉任何类型的Exception,但唯一允许您实际再次抛出的是未经检查的。 (这就是为什么将未经检查的异常转换为检查的异常会破坏您的代码。)

使用Exception 捕获未经检查的异常是有效的,因为未经检查的异常(又名RuntimeExceptions)是异常的子类,它遵循标准的多态规则;它不会将捕获的异常转换为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_errorstd::logic_errorstd::bad_alloc、任何正确定义的用户创建的异常等等,因为它们都派生自 std::exception

tl;dr:你没有捕捉到 checked 异常,你捕捉到了 any 异常。只有将异常转换为已检查的异常类型时,该异常才会成为已检查的异常。

【讨论】:

@AdamSkywalker 请不要对只试图帮助你的人轻率。多态性论证是完全有效的,因为它的核心 catch (Exception ex) 非常像 Exception ex = ...

以上是关于为啥不抛出异常的代码允许捕获已检查的异常?的主要内容,如果未能解决你的问题,请参考以下文章

C++ STL栈问题:为啥栈为空时pop()不抛出异常?

Java中的异常处理:何时抛出异常,何时捕获异常?

为啥设置 DataSource 时 ComboBox 不抛出异常?

spring事物回滚机制 (事务异常回滚,捕获异常不抛出就不会回滚)

为啥 JPA 重复持久方法不抛出异常?

Spring事务异常回滚,捕获异常不抛出就不会回滚