放松异常捕获的必要性

4

在Java中是否有可能摆脱必须捕获非RuntimeException异常的需要?也许可以通过编译器标志实现?

我知道为什么要推广捕获,但希望创建简单直接的工具来强制执行它们的要求。因此,如果出现问题,我不想捕获它,而是退出应用程序,并崩溃并提示有意义的异常。通常情况下会像这样结束:

try {
     connection.close();
} catch (IOException e) {
    throw new RuntimeException(e);
}   

这段代码引入了4行混乱的代码,并在错误输出中引入了包装RuntimeException的混乱。有时甚至会鼓励人们在任何地方包装大量的try ... catch (Throwable ..)块,这很可能是我们所喜爱的“发生未知错误”的警报框的原因...


也许抛出一个更有意义的未检查异常?如果你能将这四行代码封装到一个单一的类或静态方法中,那么情况并不会太糟糕。 - Jeremy
1
听起来你可能想在你的应用程序中建立一个异常处理的约定。我读了这个问题很多遍,似乎有些地方需要真正处理异常,而另一些地方必须终止应用程序。 - Vineet Reynolds
对于在关闭时出现异常的情况,如果不太可能在异常上做任何操作,人们倾向于编写类似于IOUtils.closeQuietly但用于连接的closeQuietly方法。 - MeBigFatGuy
我喜欢Java异常的意义,所以我认为没有必要添加任何东西。每个自定义异常都会混淆或在轻微的情况下使调试输出中应用程序故障的真实来源变得复杂。 - dronus
6个回答

3
一看到异常就崩溃的应用程序是非常糟糕的做法,特别是当有一些工作尚未保存且应用程序正在使用需要在应用程序终止执行之前被释放和清理的一些资源时。一些非常受欢迎的软件曾经这样做...但是他们只是在应用程序重新启动时引入了数据可恢复性功能,而没有“修复”问题。然而,无论如何,这都不是良好的软件工程。

至少,你的应用程序不应该在第一个遇到的异常/错误上崩溃,而应该用有意义的消息恢复。将所有东西都包装在一个 RuntimeException(或者甚至 Throwable)中并且什么也不做是懒惰的行为。

Java不支持任何类型的标志,因为有1)解决方法,2)更好的处理此情况的方法。例如:

1. 在调用方法中处理异常

您可以在方法声明中添加 throws 关键字,直到您的 static public void main 方法,如果不处理异常,最终会通过堆栈跟踪崩溃应用程序。

class Foo {
   public void someMethod(....) throws IllegalArgumentException, IOException {
      ...
   }

   static public void main(String...args) throws Throwable {
      new Foo().someMethod();
   } 
}

这种方法没有提供任何可恢复性的手段,可能会让用户感到不满(如果他们从控制台运行应用程序,则出现无意义的大堆栈跟踪,如果他们从快捷方式或GUI启动,则什么也没有)。此外,如果您拥有一些已获取的资源,当异常发生时将无法清理它们。至少,您的 main 应该 catch (Throwable e) 并在抛出上述异常之前输出一些内容。例如:

class Foo {
   public void someMethod(....) throws IllegalArgumentException, IOException {
      ...
   }

   static public void main(String...args) {
      try {
         new Foo().someMethod();
      } catch (...) {
         // output or log exception here and, optionally, cleanup and exit
      }
   } 
}

**编辑**

考虑这种情况:一个程序正在初始化一些资源以处理一些数据,然后在处理过程中发生了运行时异常(或错误),应用程序崩溃,但是资源没有被释放或释放。然而,在Java中,可以这样做:

public E doSomething() throws RuntimeException {
   // declare a bunch of resources

   try {
      // process resources with unchecked exceptions
   } finally {
      // free resources
   }

   // return some result
}

在错误或成功时,可以干净地退出方法,甚至为了“纪念”记录运行时错误。

2. 记录错误并返回有意义的值

记录日志是一种非常好的实践。您可以向用户显示一些消息,告诉他们操作无法执行而不会崩溃整个系统,并提供一些跟踪信息,以了解用户正在进行什么操作和位置。简单的日志记录系统可能如下:

class Foo {
   static private final Logger LOG = Logger.getLogger(Foo.class.getName());

   public boolean doSomethingImpl(...) {
      boolean result = true;
      try {
        ...
      } catch (SomeException e) {
         LOG.log(Level.SEVERE, "meaningful message why method could not do something!", e);
         result = false;
      }
      return result;
   }

   public void doSomething() {
      if (!doSomethingImpl(...)) {
          // handle failure here
      }
   }
}

默认情况下,Logger 会将所有内容输出到 err 输出流中,但是您可以添加自己的处理程序:

// loggers are singletons, so you can retrieve any logger at anytime from
// anywhere, as long as you know the logger's name
Logger logger = Logger.getLogger(Foo.class.getName());

logger.setUseParentHandlers(false);  // disable output to err
logger.addHandler(new MyHandler());  // MyHandler extends java.util.logging.Handler

Java已经内置了一些默认的日志处理程序,其中之一写入文件

等等。


在第一次出现异常时崩溃应用程序是非常糟糕的做法,特别是当有些工作还没有保存时。但是,在编写小的命令行应用程序或类似插件的可执行文件时,没有工作需要丢失,每一行不必要的代码或损坏的调试输出对我来说都是不好的实践。Java异常错误输出及其代码位置对我来说很好。一些其他可靠的工具也是这样工作的,例如rsync,它在失败时记录代码文件和行号。 - dronus
1
我理解对于小工具/插件等,或许可以这样做。然而,我仍然认为即使不是必须的,也应该养成良好的习惯。没有一个程序是适合糟糕代码的。在我看来,无论如何都是如此。 - Yanick Rochon
我在编写好代码的过程中遇到了这个问题... 我偏爱简约、小巧的代码,因此我不喜欢Java对某些“异常”进行预定义检查的强制执行。 - dronus
@dronus,我认为您并不完全理解异常的要点。Java之所以有异常,是因为有很好的原因。其中之一是“异常是程序执行期间发生的事件,它会中断正常的程序指令流程”。异常使得在不同的可能事件下进行分支成为可能;这与简单地返回null(或0),或者依赖于调用另一个方法来查找何时/为什么/在哪里是不同的。我建议您阅读整个部分,尤其是http://download.oracle.com/javase/tutorial/essential/exceptions/runtime.html - Yanick Rochon
这正是我想要的。在出现某些问题时,程序指令的正常流程应该被打断,而在我的情况下,程序应该立即终止,并将问题描述到 stderr。但是 Java 强制我捕获一些异常并自行进行特殊处理。因此,如果我决定在这些异常上退出程序,我必须编写额外的重新抛出代码,并且结果的 stderr 输出通常与未捕获的 RuntimeException 不同。这就是我不喜欢的整个事实,我想摆脱它,但似乎不可能。 - dronus

3

你可以在方法原型中使用throws关键字来避免try-catch块。这最终将异常抛给JVM的默认异常处理程序,如果你的代码中没有指定捕获块来处理引发的异常,则会使应用程序停止。


如果方法覆盖了一个非throws方法,如Thread.run(),则无法工作。 - dronus

1

你确实有能力将异常抛到更高的层次。这里是一个例子

public class Foo {
    public Foo() {
        super();
    }
    public void disconnect(connection) throws IOException {
        connection.close();
    }
}

如果覆盖框架方法,如Thread.run(),则不可能实现这一点。 - dronus
啊,谢谢,我之前不知道这个。以前没有真正遇到过线程管理。 - Michael

1

我认为对于JVM来说,没有什么绕过这个问题的方法。你最好的选择是让你的方法重新抛出异常,这样可以清除代码中的“混乱”,然后让你的主程序抛出异常。这将传播错误到程序的顶部。

请记住,异常实际发生的地方是让用户知道发生了什么的更好的地方(即,在特定的IOException发生时正在执行什么操作)。如果所有错误都简单地传播到顶层,你将失去这种分辨率。


如果覆盖框架方法,则通常无法使用“最好的选择是重新抛出异常”的方法。例如,Thread.run()方法不允许将异常抛出。 - dronus

1
使用“Throws”可以避免错误,但这不是良好的编程实践。

1
Java中是否有可能摆脱必须捕获非运行时异常的必要性?
对于已检查异常,您可以选择捕获异常或在方法头中声明抛出异常。
也许有编译器标志吗?
不。没有编译器标志来放宽这个限制。这是语言设计的基本部分。通过编译器开关放松已检查异常规则将会导致严重的库互操作性问题。

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