“error: unreported exception <XXX>; must be caught or declared to be thrown” 是什么意思,如何修复?

5
新的Java程序员经常会遇到类似于这样的错误提示:
"error: unreported exception <XXX>; must be caught or declared to be thrown" 

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

请解释:

  • 编译错误信息表明了什么,
  • 这个错误背后的Java概念,以及
  • 如何修复它。

2
请注意。这是针对此类问题的规范问答。虽然已经有很多(数百)现有的问题,但它们要么包含“太多代码”,要么答案过于特定于代码。 - Stephen C
1个回答

8

首先要明确的是,这是编译错误而不是异常。你应该在编译时看到它。

如果你在运行时看到了它出现在异常信息中,那很可能是因为你正在运行一些存在编译错误的代码。回去修复编译错误,然后找到并设置你的IDE中的选项,以防止它为存在编译错误的源代码生成“.class”文件。(节省未来的痛苦)


回答这个问题的简短答案是:

  • 错误信息提示该语句抛出(或传播)了一个已检查异常,但是异常(XXX)没有被正确处理。

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

    • 使用try…catch语句捕获并处理异常,或者
    • 声明封闭的方法或构造函数会throws1

1- 在某些边缘情况下可能无法这样做。请阅读其他答案!


已检查异常与未检查异常

在Java中,异常由继承自java.lang.Throwable类的类表示。异常分为两类:

  • 已检查异常是指ThrowableException及其子类,除了RuntimeException及其子类。
  • 未检查异常是指所有其他异常;即Error及其子类和RuntimeException及其子类。

(在上述情况中,“子类”包括直接和间接子类。)

已检查异常和未检查异常之间的区别在于,必须在发生异常的封闭方法或构造函数内处理已检查异常,但无需处理未检查异常。

(问题:如何确定异常是否已检查?答案:找到异常类的javadoc,并查看其父类。)

如何处理(已检查)异常

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

你可以在 try ... catch 语句中捕获异常。例如:
public void doThings() {
    try {
        // 做一些事情
        if (someFlag) {
            throw new IOException("无法读取某些内容");
        }
        // 做更多的事情
    } catch (IOException ex) {
        // 处理它 <<<=== 在这里
    }
}

在上面的代码中,我们将抛出(已检查的)IOException 的语句放在了 try 的主体中。然后,我们编写了一个 catch 子句来捕获异常。(我们可以捕获 IOException 的超类...但在这种情况下,那将是 Exception,而捕获 Exception 是不好的做法。)
你可以声明封闭方法或构造函数 throws 异常:
public void doThings() throws IOException {
    // 做一些事情
    if (someFlag) {
        throw new IOException("无法读取某些内容");
    }
    // 做更多的事情
}  

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

这些事情中哪一个是正确的做法?

这取决于上下文。然而,一个通用原则是,您应该在能够适当地处理异常的代码级别上处理异常。这又取决于异常处理代码将要执行的操作(在此处)。它能够恢复吗?它可以放弃当前请求吗?它应该停止应用程序吗?

解决问题

回顾一下。编译错误意味着:

  • 您的代码抛出了一个已检查的异常,或者调用了一些抛出已检查异常的方法或构造函数,并且
  • 它没有通过捕获异常或按照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");
    }
}

如果您一直在关注我们之前所说的内容,您会意识到t()将会出现“未报告异常”的编译错误。在这种情况下,错误在于t已被声明为throws IllegalAccessException。实际上,异常不会传播,因为它已经在抛出它的方法中被捕获。

在这个例子中,修复的方法是删除throws IllegalAccessException

这里的小课程是,throws IllegalAccessException是该方法表示调用者应该期望异常传播。它并不意味着它会传播。反过来,如果您希望异常传播(例如,因为它没有被抛出,或者因为它被捕获并且没有重新抛出),那么该方法的签名就不应该说它被抛出了!

异常的不良实践

有几件事情是您应该避免做的:

  • 不要捕获 Exception(或 Throwable)作为捕获异常列表的捷径。如果这样做,你可能会捕获到意外的异常(如未经检查的 NullPointerException),然后试图在不应该恢复时进行恢复。

  • 不要将方法声明为 throws Exception。这会强制调用者处理(潜在的)任何已检查异常...这是一场噩梦。

  • 不要压制异常。例如:

    try { 
        ...
    } catch (NullPointerException ex) {
        // It never happens ... ignoring this
    }
    

    如果你压制异常,你可能会使触发它们的运行时错误更难诊断。你正在销毁证据。

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

边缘情况:静态初始化器

有些情况下,处理已检查异常是一个问题。其中一个特殊情况是在static初始化器中处理已检查异常。例如:

private static final FileInputStream input = new FileInputStream("foo.txt");

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'
}

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

边缘情况:静态块

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

边缘情况:Lambda表达式

Lambda表达式(通常)不应抛出未经检查的异常。这不是对Lambda表达式本身的限制。而是由于用于参数的函数接口的后果,在您提供参数的地方。除非函数声明了已检查的异常,否则Lambda不能抛出异常。例如:

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

尽管我们似乎已经捕获了IOException,但编译器会抱怨:

  • lambda表达式中存在未捕获的异常,而且
  • catch捕获了从未抛出的异常!

实际上,异常需要在lambda表达式本身中捕获:

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

(敏锐的读者会注意到,在delete抛出异常的情况下,这两个版本的行为是不同的...)

更多信息

Oracle Java 教程:


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