何时选择已检查和未检查异常

267
在Java(或其他具有已检异常的语言)中,当创建自己的异常类时,如何决定它应该是已检异常还是未检异常?
我的直觉是,在调用者可能可以以某种有益的方式恢复的情况下,应该使用已检异常,而未检异常则更适用于不可恢复的情况,但我很想听听其他人的想法。

15
Barry Ruzek撰写了一份优秀指南,介绍如何选择使用已检查异常或未检查异常。 - sigget
18个回答

306

检查异常只有在您理解何时应该使用它们时才会非常好。Java核心API未遵循这些规则,导致SQLException(有时也包括IOException)非常糟糕。

可检查异常应用于可预测的无法防止的错误,这些错误合理地可以恢复

不受检查的异常适用于其他所有情况。

我将为您解释一下,因为大多数人误解了这个意思。

  1. 可预测的但无法防止的:调用者尽了最大努力验证输入参数,但是某些超出其控制的条件导致操作失败。例如,您尝试读取一个文件,但是有人在您检查它是否存在和读取操作开始之间删除了它。通过声明可检查的异常,您告诉调用者要预期此故障。
  2. 合理地可以恢复:告诉调用者要预期他们无法恢复的异常没有任何意义。如果用户尝试从不存在的文件中读取数据,则调用者可以提示他们提供新的文件名。另一方面,如果方法由于编程错误(无效的方法参数或有缺陷的方法实现)而失败,则应用程序无法在执行过程中修复问题。它所能做的最好的事情就是记录该问题并等待开发人员在以后的时间内修复它。

除非您要抛出的异常满足上述所有条件,否则应使用不受检查的异常。

在每个级别重新评估:有时捕获检查异常的方法不是处理错误的正确位置。在这种情况下,请考虑对您自己的调用者来说合理的内容。如果异常是可预测的、无法防止的,并且他们可以合理地从中恢复,那么您应该自己抛出可检查的异常。否则,您应该将异常包装在不受检查的异常中。如果遵循此规则,您将发现自己根据所处的层将可检查的异常转换为不受检查的异常,反之亦然。

不论是检查异常还是未检查异常,都应该使用正确的抽象级别。例如,具有两个不同实现(数据库和文件系统)的代码仓库应该避免通过抛出SQLExceptionIOException来暴露实现特定细节。相反,它应该将异常包装在跨越所有实现的抽象中(例如RepositoryException)。


3
“你尝试读取一个文件,但在你检查它是否存在和读操作开始之间,有人将其删除。”=> 这怎么可能是“预期”的呢?对我来说更像是意料之外且可以避免的事情。谁会预料到一个文件会在两个语句之间被删除呢? - Koray Tugay
13
“Expected”并不意味着这种情况是典型的。它只是意味着我们可以预见到这种错误发生(相对于无法预测的编程错误)。“Unpreventable”指的是程序员无法防止用户或其他应用程序在我们检查文件是否存在和读取操作开始之间删除文件的事实。 - Gili
那么方法内的任何与数据库相关的问题都必须抛出已检查异常吗? - ivanjermakov
1
只有当方法的用户期望该方法与数据库相关时,才应该这样做。如果您有一个 Point::getX() 方法,那么在数据库中查找它可能是意外的;同样意外的是,该点可能没有 X 值。首先定义接口,然后考虑 - 实现独立 - 可能出错的情况。 - derM
你说了怎么做,但没有解释为什么。 - choxsword
@choxsword 这个问题并没有问为什么。我的观点是,受检异常是 Java 强类型化文化的结果。我们希望在编译时尽可能捕获多余的错误,而不是像 JavaScript 一样在运行时出现异常。对于那些可预测且可预防的少数特殊情况,我们希望用户“勾选列表”并自觉地决定如何处理每种情况。 - Gili

64

来自一位Java初学者的话:

当发生异常时,你必须要么捕获和处理异常,要么告诉编译器你无法处理它,通过声明你的方法会抛出该异常,然后使用你方法的代码将不得不处理该异常(即使它也可以选择声明会抛出异常,如果它无法处理它)。

编译器将检查我们是否做了以上两种操作之一(捕获或声明)。 所以这些被称为Checked exceptions。但Errors和Runtime Exceptions不会被编译器检测到(即使您可以选择捕获或声明,也不是必需的)。 因此,这两者被称为Unchecked exceptions。

Errors用于表示发生在应用程序外部的条件,例如系统崩溃。 Runtime exceptions通常由应用程序逻辑错误引起。 在这些情况下你什么也无法做。 当运行时异常发生时,你必须重新编写程序代码。因此,编译器不会检查这些exception。这些runtime exceptions将在开发和测试期间暴露出来。 然后我们必须重构我们的代码以消除这些错误。


16
那是传统观点,但是有很多争议。 - artbristol

56
我遵循的规则是:永远不要使用未经检查的异常!(或者当你找不到解决方法时才用)。
有人持相反意见:永远不要使用已检查的异常。我不愿意在这个辩论中站队(双方都有好的观点!)但是许多专家认为,在事后看来,已检查的异常是一个错误的决定。
有关一些讨论,请查看WikiWikiWeb的“已检查的异常的价值存疑”页面。另一个早期、广泛的论点示例是Rod Waldhoff的博客文章

6
在我看来,如果有一种简单的方法让一个方法声明它在特定代码块中不希望调用的方法抛出某些异常(甚至没有任何异常),并且任何与此期望相违背的已检查异常都应该被包装在其他异常类型中并重新抛出,那么检查异常本可以成为一项重要的资产。我想说,在代码没有准备好处理已检查异常的情况下,90%的时间使用包装和重新抛出是处理它们的最佳方式,但由于缺乏语言支持,这种方式很少被采用。 - supercat
2
我对异常设计的一个不满是,即使 foo 被记录为在读取文件结尾时抛出 barException,但如果 foo 调用了一个抛出 barException 的方法,即使 foo 不希望这样做,调用 foo 的代码也会认为已经到达了文件结尾,并且不知道发生了意外情况。我认为这种情况应该是检查异常最有用的情况之一,但这也是编译器允许未处理检查异常的唯一情况。 - supercat
@supercat:第一条评论中已经有一个简单的方法可以实现你想要的功能:将代码放在try块中,捕获异常,并将异常包装在RuntimeException中重新抛出。 - Warren Dew
@KonradRudolph:确实。好的编程语言之一的标志是,它们使得做正确的事情比做错误的事情更容易。在Java中正确地处理意外异常并不是不可能的,但肯定比仅仅希望它们永远不会以使调用者将意外条件误认为预期条件的方式更加困难。 - supercat
1
@KonradRudolph的supercat提到了“一个特定的代码块”; 鉴于该块必须被定义,声明性语法不会显著减少膨胀。如果您认为整个函数都应该是声明性的,那么这将鼓励糟糕的编程,因为人们只会插入声明而不是实际查看可能捕获的已检查异常,并确保没有其他更好的处理方式。 - Warren Dew
显示剩余2条评论

52
在任何足够庞大、有许多层级的系统中,受检异常都是无用的,因为你需要一个架构层次的策略来处理异常(使用故障隔离)。
使用受检异常会使你的错误处理策略被微观管理,并且在任何大型系统上都无法承受。
大多数情况下,你不知道错误是否可恢复,因为你不知道调用你的API的调用方位于哪个层级。
比如说,我创建了一个StringToInt API,将整数的字符串表示转换为Int。如果以"foo"字符串调用API,我必须抛出受检异常吗?它是否可恢复?我不知道,因为在他的层级中,我的StringToInt API的调用方可能已经验证了输入,如果抛出此异常,则是一个bug或数据损坏,对于该层级而言是不可恢复的。
在这种情况下,API的调用方并不想捕获异常。他只想让异常“冒泡”。如果我选择了受检异常,那么这个调用方将有很多无用的catch块,只是为了人为地重新抛出异常。
什么是可恢复的往往取决于API的调用方,而不是API的编写者。API不应该使用受检异常,因为只有未受检异常才允许选择捕获或忽略异常。

4
这很接近于http://userstories.blogspot.com/2008/12/checked-exception-why-debat-is-not-over.html。 - alexsmail
22
互联网总是让我感到惊喜,实际上这是我的博客 :) - Stephane

34

你是正确的。

未检查的异常用于让系统快速失败,这是一件好事。你应该清楚地说明方法需要什么才能正常工作。这样你就只需验证输入一次。

例如:

/**
 * @params operation - The operation to execute.
 * @throws IllegalArgumentException if the operation is "exit"
 */
 public final void execute( String operation ) {
     if( "exit".equals(operation)){
          throw new IllegalArgumentException("I told you not to...");
     }
     this.operation = operation; 
     .....  
 }
 private void secretCode(){
      // we perform the operation.
      // at this point the opreation was validated already.
      // so we don't worry that operation is "exit"
      .....  
 }

举个例子,如果系统快速失败,那么您将知道它在哪里以及为什么失败。您将获得类似于以下的堆栈跟踪:

 IllegalArgumentException: I told you not to use "exit" 
 at some.package.AClass.execute(Aclass.java:5)
 at otherPackage.Otherlass.delegateTheWork(OtherClass.java:4569)
 ar ......

你会知道发生了什么。在“delegateTheWork”方法中(位于第4569行),OtherClass使用“exit”值调用了你的类,即使它不应该等等。

否则,你将不得不在代码中到处添加验证,这样容易出错。此外,有时很难追踪出错原因,你可能需要花费数小时进行令人沮丧的调试。

NullPointerExceptions也会发生同样的情况。如果你有一个700行的类,其中包含15个方法,使用30个属性,其中没有一个属性可以为null,那么你可以将所有这些属性设置为只读,并在构造函数或工厂方法中进行验证,而不是在每个方法中验证其可空性。

 public static MyClass createInstane( Object data1, Object data2 /* etc */ ){ 
      if( data1 == null ){ throw NullPointerException( "data1 cannot be null"); }

  }


  // the rest of the methods don't validate data1 anymore.
  public void method1(){ // don't worry, nothing is null 
      ....
  }
  public void method2(){ // don't worry, nothing is null 
      ....
  }
  public void method3(){ // don't worry, nothing is null 
      ....
  }
已检查异常在程序员(你或你的同事)做了一切正确的事情,验证了输入,运行了测试,所有代码都完美无缺,但代码连接到可能关闭的第三方Web服务(或者你正在使用的文件被另一个外部进程删除等)时非常有用。甚至在尝试连接之前,Web服务可能已经过验证,但在数据传输期间出现了问题。

在这种情况下,你或你的同事无法帮助它。但是你仍然必须做些什么,不能让应用程序只是死掉并消失在用户的眼中。你可以使用已检查异常来处理异常,当发生异常时,你能做什么?大多数情况下,只需要尝试记录错误,可能保存你的工作(应用程序工作),并向用户呈现一条消息(网站blabla已关闭,请稍后重试等)。

如果过度使用已检查异常(通过在所有方法签名中添加“throw Exception”),那么你的代码将变得非常脆弱,因为每个人都会忽略该异常(因为太普遍了),代码质量将严重受损。

如果过度使用未检查异常,类似的情况也会发生。那段代码的用户不知道是否会出现问题,并且会出现很多try{...}catch(Throwable t)。


2
说得好!+1。总是让我惊讶的是,调用者(未检查)/被调用者(已检查)的区别不够明显... - VonC

24

这是我的"经验法则"。

  • 对于由调用方引起的失败(涉及显式和完整的文档),我在方法代码中使用未经检查的异常
  • 对于由被调用者引起的失败,我需要明确告知任何想要使用我的代码的人,因此使用已检查的异常

与先前的答案相比,这是使用一种或两种异常的清晰理由(可以同意或不同意)。


对于这两种异常,我将为我的应用程序创建自己的未检查和已检查异常(一个好的做法,如此处所述),除了非常常见的未检查异常(例如NullPointerException)。
因此,例如,下面这个特定函数的目标是制作(或获取如果已存在)一个对象,意味着:
  • 对象的容器必须存在(调用者的责任=>未检查异常,并且这个被调用函数有清晰的javadoc注释)
  • 其他参数不能为空(编码人员选择将其放在调用者身上:编码人员不会检查空参数,但编码人员确实记录了它)
  • 结果不能为空(被调用代码的责任和选择,这个选择对于调用者非常重要=>已检查异常,因为每个调用者都必须决定是否可以创建/找到对象,并且该决策必须在编译时强制执行:他们不能使用此函数而不必处理这种可能性,也就是说,处理这个已检查异常)。
示例:
/**
 * Build a folder. <br />
 * Folder located under a Parent Folder (either RootFolder or an existing Folder)
 * @param aFolderName name of folder
 * @param aPVob project vob containing folder (MUST NOT BE NULL)
 * @param aParent parent folder containing folder 
 *        (MUST NOT BE NULL, MUST BE IN THE SAME PVOB than aPvob)
 * @param aComment comment for folder (MUST NOT BE NULL)
 * @return a new folder or an existing one
 * @throws CCException if any problems occurs during folder creation
 * @throws AssertionFailedException if aParent is not in the same PVob
 * @throws NullPointerException if aPVob or aParent or aComment is null
 */
static public Folder makeOrGetFolder(final String aFoldername, final Folder aParent,
    final IPVob aPVob, final Comment aComment) throws CCException {
    Folder aFolderRes = null;
    if (aPVob.equals(aParent.getPVob() == false) { 
       // UNCHECKED EXCEPTION because the caller failed to live up
       // to the documented entry criteria for this function
       Assert.isLegal(false, "parent Folder must be in the same PVob than " + aPVob); }

    final String ctcmd = "mkfolder " + aComment.getCommentOption() + 
        " -in " + getPNameFromRepoObject(aParent) + " " + aPVob.getFullName(aFolderName);

    final Status st = getCleartool().executeCmd(ctcmd);

    if (st.status || StringUtils.strictContains(st.message,"already exists.")) {
        aFolderRes = Folder.getFolder(aFolderName, aPVob);
    }
    else {
        // CHECKED EXCEPTION because the callee failed to respect his contract
        throw new CCException.Error("Unable to make/get folder '" + aFolderName + "'");
    }
    return aFolderRes;
}

20

问题不仅仅是能否从异常中恢复。在我看来,更重要的是调用者是否有兴趣捕获异常。

如果你编写一个将在别处使用的库或应用程序的较低层级,请问一下自己:调用者是否有兴趣捕获(知道)你的异常?如果他没有兴趣,则使用未经检查的异常,这样您就不会给他造成不必要的负担。

许多框架都采用了这种哲学。特别是Spring和Hibernate - 它们将已知的已检查异常转换为未经检查的异常,因为Java中过度使用了已检查异常。我能想到的一个例子是来自json.org的JSONException,它是一个已检查异常,但大多数情况下只是麻烦 - 它应该是未经检查的,但开发人员简单地没有认真考虑过。

顺便说一句,大多数情况下,调用者对异常的兴趣与从异常中恢复的能力直接相关,但并非总是如此。


14

这是一个非常简单的解决方案,可以解决您Checked/Unchecked难题。

规则1:将未检查的异常视为代码执行前可测试的条件。例如...

x.doSomething(); // the code throws a NullPointerException

如果 x 为 null,代码应该可能具有以下内容:

if (x==null)
{
    //do something below to make sure when x.doSomething() is executed, it won’t throw a NullPointerException.
    x = new X();
}
x.doSomething();

规则2:将Checked Exception视为代码执行时可能出现的无法测试的条件。
Socket s = new Socket(“google.com”, 80);
InputStream in = s.getInputStream();
OutputStream out = s.getOutputStream();

在上面的例子中,由于DNS服务器宕机,URL(google.com)可能无法使用。即使DNS服务器工作并将“google.com”名称解析为IP地址,如果在此之后的任何时候连接到google.com,网络都可能会崩溃。在读写流之前,您根本无法一直测试网络。
有时代码必须在我们知道是否存在问题之前执行。通过强制开发人员以强制他们通过Checked Exception来处理这些情况的方式编写其代码,我必须向发明这个概念的Java创建者致敬。
通常,Java中的几乎所有API都遵循上述2个规则。如果尝试写入文件,则在完成写入之前,磁盘可能已填满。其他进程可能导致磁盘变满。根本没有办法测试这种情况。对于与硬件交互的人,在任何时候使用硬件都可能失败,Checked Exceptions似乎是解决此问题的一种优雅方法。
这方面存在一些灰色区域。如果需要许多测试(带有大量&&和||的令人震惊的if语句),则抛出的异常将是CheckedException,因为很难正确处理-您无法说这个问题是编程错误。如果少于10个测试(例如,“if(x == null)”),则程序员错误应该是UncheckedException。
在处理语言解释器时,事情变得有趣。根据上述规则,语法错误应被视为Checked或Unchecked Exception?我认为,如果可以在执行之前测试语言的语法,则应将其视为UncheckedException。如果无法测试语言-类似于汇编代码在个人计算机上运行的方式,则Syntax Error应为Checked Exception。
以上2个规则可能会消除您90%的关注点。要总结这些规则,请遵循以下模式... 1)如果要执行的代码可以在执行之前进行测试以使其正确运行,并且发生异常(即程序员错误),则应该是UncheckedException(RuntimeException的子类)。 2)如果要执行的代码无法在执行之前进行测试以使其正确运行,则应该是Checked Exception(Exception的子类)。

9
您可以将其称为已检查或未检查的异常;但是,程序员可以捕获所有类型的异常,因此最好的答案是:将所有异常编写为未检查并对其进行文档化。这样使用您的 API 的开发人员可以选择是否要捕获该异常并执行某些操作。已检查的异常完全浪费了所有人的时间,使您的代码变得非常混乱。适当的单元测试将引发可能需要捕获并处理的任何异常。

1
+1 提到单元测试可能是解决检查异常意图解决的问题的更好方法。 - Keith Pinson
单元测试加1分。使用已检查/未检查异常对代码质量影响很小。因此,如果使用已检查异常会导致更好的代码质量的论点是完全无效的论点! - user1697575

8

可检查异常(Checked Exception): 如果客户端可以从异常中恢复并且希望继续执行,使用可检查异常。

不可检查异常(Unchecked Exception): 如果客户端在异常后无法继续执行任何操作,则引发不可检查异常。

例如:如果您需要在方法A()中执行算术操作,并且根据A()的输出结果,您必须执行另一种操作。如果在运行时从方法A()中收到了您不希望看到的null值,则应抛出Null pointer Exception,它是运行时异常。

请参阅此处


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