“错误:未报告的异常 <XXX>;必须捕获或声明抛出”是什么意思以及如何修复它?

Ste*_*n C 5 java compiler-errors uncaught-exception checked-exceptions

新的 Java 程序员经常遇到如下错误:

"error: unreported exception <XXX>; must be caught or declared to be thrown" 
Run Code Online (Sandbox Code Playgroud)

其中 XXX 是某个异常类的名称。

请解释:

  • 编译错误消息的内容是什么,
  • 此错误背后的 Java 概念,以及
  • 如何修复它。

Ste*_*n C 8

首先要事。这是一个编译错误,并非例外。您应该在编译时看到它。

如果您在运行时异常消息中看到它,那可能是因为您正在运行一些包含编译错误的代码。返回并修复编译错误。然后在 IDE 中找到并设置防止它为存在编译错误的源代码生成“.class”文件的设置。(为自己节省未来的痛苦。)


这个问题的简短回答是:

  • 错误消息表明,出现此错误的语句正在抛出(或传播)已检查的异常,并且该异常(该XXX)未得到正确处理。

  • 解决方案是通过以下任一方式处理异常:

    • try ... catch语句捕获并处理它,或者
    • 声明封闭方法或构造throws函数1

1 - 在某些极端情况下您无法这样做。阅读答案的其余部分!


检查异常与非检查异常

在 Java 中,异常由该类的派生类来表示java.lang.Throwable。异常分为两类:

  • 检查异常Throwable、 和Exception及其子类,除了RuntimeException和 其子类。
  • 未经检查的异常是所有其他异常;即Error及其子类,RuntimeException及其子类。

(上面的“子类”包括直接子类和间接子类。)

检查异常和非检查异常之间的区别在于,检查异常必须在它们发生的封闭方法或构造函数内“处理”,但不需要处理非检查异常。

(问:如何知道异常是否被检查?答:找到异常类的 javadoc,并查看其父类。)

你如何处理(已检查的)异常

从Java语言的角度来看,有两种处理异常的方法可以让编译器“满意”:

  1. 您可以在语句中捕获异常try ... catch。例如:

    public void doThings() {
        try {
            // do some things
            if (someFlag) {
                throw new IOException("cannot read something");
            }
            // do more things
        } catch (IOException ex) {
            // deal with it    <<<=== HERE
        }
    }
    
    Run Code Online (Sandbox Code Playgroud)

    IOException在上面,我们将抛出(checked) 的语句放在try. 然后我们写了一个catch子句来捕获异常。(我们可以捕获...的超类IOException,但在这种情况下,Exception捕获Exception是一个坏主意。)

  2. 您可以声明封闭方法或构造throws函数异常

    public void doThings() throws IOException {
        // do some things
        if (someFlag) {
            throw new IOException("cannot read something");
        }
        // do more things
    }  
    
    Run Code Online (Sandbox Code Playgroud)

    在上面我们已经声明了doThings()throws IOException。这意味着调用该doThings()方法的任何代码都必须处理异常。简而言之,我们将处理异常的问题传递给调用者。

以下哪些事情是正确的做法?

这取决于上下文。但是,一般原则是您应该在代码中能够适当处理异常的级别上处理异常。这又取决于异常处理代码将要执行的操作(位于HERE)。能恢复吗?它可以放弃当前的请求吗?是否应该停止申请?

解决问题

回顾一下。编译错误的意思是:

  • 您的代码引发了已检查的异常,或者调用了引发已检查的异常的某些方法或构造函数,并且
  • 它没有通过捕获异常或按照 Java 语言的要求声明异常来处理异常。

你的解决过程应该是:

  1. 了解异常的含义以及抛出异常的原因。阅读 javadoc 以了解异常和引发异常的方法。
  2. 根据1,阅读您的代码并决定处理可能的异常的正确方法。
  3. 在2的基础上,对自己的代码进行相关修改。

示例:以相同的方法投掷和接球

考虑本问答中的以下示例

public class Main {
    static void t() throws IllegalAccessException {
        try {
           throw new IllegalAccessException("demo");
        } catch (IllegalAccessException e){
            System.out.println(e);
        }
    }

    public static void main(String[] args){
        t();
        System.out.println("hello");
    }
}
Run Code Online (Sandbox Code Playgroud)

如果您一直遵循我们到目前为止所说的内容,您将意识到这t()将给出“未报告的异常”编译错误。在本例中,错误在于t已声明为throws IllegalAccessException。事实上,异常不会传播,因为它已在抛出它的方法中被捕获。

此示例中的修复方法是删除.throws IllegalAccessException

这里的小教训是,该throws IllegalAccessException方法表明调用者应该预期异常会传播。这实际上并不意味着它会传播。另一方面是,如果您希望异常传播(例如,因为它没有被抛出,或者因为它被捕获并且没有被重新抛出),那么该方法的签名不应该说它被抛出!

不良做法(有例外)

您应该避免做以下几件事:

  • 不要将 catch Exception(或Throwable) 作为捕获异常列表的捷径。如果你这样做,你有可能捕获到你不期望的东西(比如未经检查的 NullPointerException),然后在你不应该的时候尝试恢复。

  • 不要将方法声明为throws Exception. 这迫使被调用者处理(可能)任何已检查的异常......这是一场噩梦。

  • 不要压制异常。例如

    try { 
        ...
    } catch (NullPointerException ex) {
        // It never happens ... ignoring this
    }
    
    Run Code Online (Sandbox Code Playgroud)

    如果您抑制异常,则可能会使触发异常的运行时错误变得更难以诊断。你正在销毁证据。

    注意:仅仅相信异常永远不会发生(根据评论)并不一定会成为事实。

边缘情况:静态初始化器

在某些情况下,处理已检查的异常是一个问题。一种特殊情况是static初始化程序中的检查异常。例如:

private static final FileInputStream input = new FileInputStream("foo.txt");
Run Code Online (Sandbox Code Playgroud)

FileInputStream被声明为...,这throws FileNotFoundException是一个受检查的异常。但由于上面是一个字段声明,Java 语言的语法不允许我们将声明放在try...中catch。并且没有适当的(封闭的)方法或构造函数......因为此代码是在初始化时运行的。

一种解决方案是使用static块;例如:

private static final FileInputStream input;

static {
   FileInputStream temp = null;
   try {
       temp = new FileInputStream("foo.txt");
   } catch (FileNotFoundException ex) {
       // log the error rather than squashing it
   }
   input = temp;   // Note that we need a single point of assignment to 'input'
}
Run Code Online (Sandbox Code Playgroud)

(在实际代码中有更好的方法来处理上述场景,但这不是本示例的重点。)

边缘情况:静态块

如上所述,您可以捕获静态块中的异常。但我们没有提到的是,您必须捕获块内的已检查异常。静态块没有可以捕获已检查异常的封闭上下文。

边缘情况:lambda

lambda 表达式(通常)不应引发未经检查的异常。这并不是对 lambda本身的限制。相反,它是用于提供参数的参数的函数接口的结果。除非函数声明受检查异常,否则 lambda 不能抛出异常。例如:

List<Path> paths = ...
try {
    paths.forEach(p -> Files.delete(p));
} catch (IOException ex) {
    // log it ...
}
Run Code Online (Sandbox Code Playgroud)

即使我们看起来已经捕获了IOException,编译器也会抱怨:

  • lambda 中有一个未捕获的异常,并且
  • 正在catch捕获一个从未抛出的异常!

事实上,异常需要在 lambda 本身中捕获:

List<Path> paths = ...
paths.forEach(p -> {
                      try {
                          Files.delete(p);
                      } catch (IOException ex) {
                          // log it ...
                      }
                   }
             );
Run Code Online (Sandbox Code Playgroud)

delete(精明的读者会注意到,在 a抛出异常的情况下,两个版本的行为有所不同......)

边缘情况:两个称为 Xyzzy 的异常

我见过一个方法有一个throws Xyzzy子句的情况,编译器仍然抱怨必须Xyzzy“捕获或声明抛出”。例如:未报告的异常……必须捕获或声明抛出;尽管*抛出*关键字

如果您发现自己处于这种情况,请仔细查看异常的完全限定名称。如果完全限定名称不同,则例外情况也不同。(相信编译器所说的并尝试理解它。)

更多信息

Oracle Java 教程: