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


213

在Java(或任何其他具有检查的异常的语言)中,当创建自己的异常类时,如何决定应检查还是不检查它?

我的直觉是,在调用者可能能够以某种有生产力的方式恢复的情况下,将要求使用检查异常,而对于无法恢复的情况,未检查异常将更多,但我会对其他人的想法感兴趣。


11
Barry Ruzek写了一篇关于选择检查或未检查异常的出色指南
sigget

Answers:


241

只要您知道何时应使用检查异常,它就很棒。Java核心API未能遵循SQLException(有时甚至是IOException)的这些规则,这就是它们如此糟糕的原因。

检查异常应该用于预测的,但不可预防是错误的合理歇着

未经检查的异常应用于其他所有内容。

我将为您详细说明这一点,因为大多数人都误解了这意味着什么。

  1. 可预测但无法预防:调用方在力所能及的范围内竭尽所能来验证输入参数,但其控制范围之外的某些情况导致操作失败。例如,您尝试读取文件,但是在您检查文件是否存在与读取操作开始之间有人将其删除。通过声明一个检查的异常,您可以告诉调用者预期该失败。
  2. 合理地恢复:没有必要告诉呼叫者预见他们无法恢复的异常。如果用户尝试从不存在的文件中读取,则调用者可以提示他们输入新的文件名。另一方面,如果方法由于编程错误(无效的方法自变量或错误的方法实现)而失败,则应用程序无权解决中间执行问题。最好的办法是记录问题并等待开发人员稍后解决。

除非您抛出的异常满足以上所有条件,否则应使用Unchecked Exception。

在每个级别进行重新评估:有时,捕获已检查异常的方法不是处理错误的正确位置。在这种情况下,请考虑对您自己的呼叫者而言合理的选择。如果异常是可预测的,无法预防的且使它们从中恢复的合理,那么您应该自己抛出已检查的异常。如果不是,则应将异常包装在未经检查的异常中。如果遵循此规则,您将发现自己将检查的异常转换为未检查的异常,反之亦然,具体取决于您所在的层。

对于已检查和未检查的异常,请使用正确的抽象级别。例如,具有两个不同实现(数据库和文件系统)的代码存储库应避免通过抛出SQLException或来公开特定于实现的细节IOException。相反,它应该将异常包装在涵盖所有实现的抽象中(例如RepositoryException)。


2
“您尝试读取文件,但是在您检查文件是否存在到读取操作开始之间有人将其删除。” =>这甚至是“预期的”吗?对我来说,这听起来像是:意外的和可预防的。谁会期望仅在2条语句之间删除文件?
Koray Tugay

9
@KorayTugay 预期并不意味着这种情况很典型。这只是意味着我们可以预见到该错误会提前发生(与无法提前预测的编程错误相比)。不可预防是指这样的事实,即程序员在检查文件是否存在到读取操作开始之间,无法采取任何措施阻止用户或其他应用程序删除文件。
吉利

因此,方法中与数据库相关的任何问题都必须抛出检查异常?
ivanjermakov

59

来自Java学习者

当发生异常时,您必须捕获并处理该异常,或者通过声明您的方法抛出该异常来告诉编译器您无法处理该异常,然后使用该方法的代码将必须处理该异常(即使它也可以选择声明无法处理的异常。)

编译器将检查是否已完成以下两项操作之一(捕获或声明)。因此,这些被称为Checked异常。但是编译器不会检查错误和运行时异常(即使您可以选择捕获或声明它也不是必需的)。因此,这两个称为未检查的异常。

错误用于表示在应用程序外部发生的那些情况,例如系统崩溃。运行时异常通常是由应用程序逻辑中的错误引起的。在这种情况下,您将无能为力。当运行时异常发生时,您必须重新编写程序代码。因此,编译器不会检查它们。这些运行时异常将在开发和测试期间发现。然后,我们必须重构代码以消除这些错误。


13
这是正统的观点。但是对此有很多争议。
artbristol 2012年

49

我使用的规则是:永远不要使用未经检查的异常!(或者当您看不到周围的东西时)

相反的情况非常有力:永远不要使用检查异常。我不愿意参加辩论,但似乎已经达成广泛共识,即事后看来,引入检查例外是一个错误的决定。请不要射击信使并提及这些 论点


3
恕我直言,如果有一种简单的方法可以声明不期望在特定代码块中调用的方法抛出某些异常(或根本没有异常),并且检查到任何异常,那么检查异常可能是主要资产。与此类期望相反的异常应包装在其他异常类型中并重新抛出。我认为90%的情况下代码没有准备好处理受检查的异常,这种换行是最好的处理方式,但是因为没有语言支持,所以很少这样做。
2013年

@supercat本质上就是这样:我是严格类型检查的忠实粉丝,而检查异常是其合理的扩展。尽管我在概念上非常喜欢它们,但我已经完全放弃了检查异常。
康拉德·鲁道夫2013年

1
我对异常的设计有一个烦恼,我所描述的机制将解决该异常,即如果在读取文件末尾时foo被记录为抛出异常barException,并且foo调用了barException即使foo不期望抛出异常的方法,调用的代码foo将认为已到达文件的末尾,并且不知道发生了意外的情况。我认为这种情况是检查异常应该最有用的一种情况,但这也是编译器允许未处理的检查异常的唯一情况。
2013年

@supercat:在第一个注释中已经有一种简单的方法可以执行所需的代码:将代码包装在try块中,捕获Exception,然后将Exception包装在RuntimeException中并重新抛出。
沃伦·露

1
@KonradRudolph超级猫提到“特定的代码块”;给定必须定义的块,声明式语法不会显着减少膨胀。如果您认为这对于整个函数来说都是声明性的,那将鼓励不好的编程,因为人们只是坚持声明而不是实际查看可能捕获的检查异常,并确保没有其他更好的方法处理他们。
沃伦·露

46

在具有足够多层的足够大的系统上,已检查的异常是无用的,因为无论如何,您都需要一种体系结构级别的策略来处理异常的处理方式(使用故障屏障)

使用已检查的异常,您的错误处理策略是微管理的,在任何大型系统上都是无法承受的。

大多数情况下,您不知道错误是否“可恢复”,因为您不知道API的调用者位于哪一层。

假设我创建了一个StringToInt API,该API将整数的字符串表示形式转换为Int。如果用“ foo”字符串调用API,是否必须抛出一个检查异常?是否可以恢复?我不知道,因为在我的StringToInt API调用者的层中可能已经验证了输入,并且如果抛出此异常,则它是错误或数据损坏,并且在此层不可恢复。

在这种情况下,API的调用者不想捕获异常。他只想让异常“冒泡”。如果我选择了一个已检查的异常,则此调用方将有很多无用的catch块,只能人为地抛出该异常。

可恢复的内容大部分时间取决于API的调用者,而不取决于API的编写者。API不应使用已检查的异常,因为只有未检查的异常才允许选择捕获或忽略异常。



15
@alexsmail互联网永远不会令我惊讶,实际上这是我的博客:)
Stephane

30

没错

未检查的异常用于使系统快速故障,这是一件好事。您应该清楚地说明您的方法期望什么才能正常工作。这样,您只能验证一次输入。

例如:

/**
 * @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

19

这是我的“最终经验法则”。
我用:

  • 我的方法代码中的未经检查的异常由于调用程序导致失败)(涉及显式且完整的文档
  • 由于被调用方而导致失败检查异常,我需要向任何想要使用我的代码的人明确说明

与先前的答案相比,这是使用一种或另一种(或两者)例外的明确理由(据此可以同意或不同意)。


对于这两种异常,我都会为我的应用程序创建自己的未检查和检查后的异常(一种很好的做法,如此处所述),但非常常见的未检查的异常(如NullPointerException)

因此,例如,下面这个特殊功能的目标是创建(或获取,如果已经存在)一个对象,
这意味着:

  • 使/获取的对象的容器必须存在(CALLER的责任
    =>未检查的异常,并为此调用函数清除javadoc注释)
  • 其他参数不能为空
    (编码器可以选择将其放在CALLER上:编码器将不检查空参数,但编码器会记录文件)
  • 结果不能为NULL
    (被调用方的责任和选择代码,对于调用方来说非常有意义的选择
    =>已检查的异常,因为每个调用方都必须决定是否无法创建/找到对象,并且必须在编译时强制执行该决定:他们必须先使用此函数,而不必处理这种可能性,即带有此检查的异常)。

例:


/**
 * 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;
}

19

这不仅仅是从异常中恢复的能力。在我看来,最重要的是调用方是否有兴趣捕获异常。

如果您编写要在其他地方或应用程序中较低层使用的库,请问问自己调用者是否有兴趣捕获(了解)您的异常。如果他不是,请使用不受检查的异常,这样就不会给他带来不必要的负担。

这是许多框架使用的哲学。尤其是想到Spring和Hibernate时,它们正是将已知的已检查异常转换为未检查异常,这恰恰是因为Java中过度使用了已检查异常。我可以想到的一个示例是json.org中的JSONException,它是一个已检查的异常,并且通常很烦人-应该将其取消检查,但是开发人员根本没有考虑过。

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


13

这是解决“已检查/未检查”难题的非常简单的解决方案。

规则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:将检查异常视为代码执行期间可能发生的不可测试条件。

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条规则。如果尝试写入文件,则磁盘可能在完成写入之前已装满。其他进程可能导致磁盘已满。根本没有办法测试这种情况。对于那些在任何时候与硬件进行交互的人来说,使用硬件可能会失败,检查异常似乎是解决此问题的理想方法。

这里有一个灰色区域。如果需要进行许多测试(if语句中包含许多&&和||的情况令人不寒而栗),则抛出的异常将是CheckedException,仅仅是因为它太难了而无法正确执行-您根本无法说出这个问题是编程错误。如果少于10个测试(例如'if(x == null)'),则程序员错误应该是UncheckedException。

与语言口译员打交道时,事情变得很有趣。根据上述规则,应将语法错误视为已检查异常还是未检查异常?我认为如果该语言的语法可以在执行之前进行测试,则应为UncheckedException。如果无法测试该语言(类似于汇编代码在个人计算机上的运行方式),则语法错误应为“检查异常”。

上面的2条规则可能会消除您90%的选择担忧。要总结规则,请遵循以下模式…1)如果要执行的代码可以在执行之前对其进行测试以使其正确运行,并且如果发生异常(又称为程序员错误),则该异常应该是UncheckedException(RuntimeException的子类) )。2)如果要执行的代码在执行之前无法进行测试以使其正常运行,则异常应为Checked Exception(Exception的子类)。


9

您可以将其称为已检查或未检查的异常。但是,程序员都可以捕获两种类型的异常,因此最佳答案是:将所有异常写为未检查并记录下来。这样,使用您的API的开发人员可以选择是否要捕获该异常并执行某些操作。受检查的异常完全浪费了每个人的时间,这使您的代码成为令人震惊的噩梦。正确的单元测试将提出您可能必须捕获并执行某些操作的所有异常。


1
+1值得一提的是,单元测试可能是解决已检查异常旨在解决的问题的更好方法。
基思·品森

+1用于单元测试。使用Checked / Uncheked异常对代码质量影响很小。因此,如果有人使用检查的异常会导致更好的代码质量的论点就是伪造的论点!
user1697575

7

Checked Exception: 如果客户端可以从异常中恢复并希望继续,请使用Checked Exception

未检查的异常: 如果客户端在发生异常后无法执行任何操作,则引发未检查的异常。

示例:如果希望您在方法A()中基于A()的输出进行算术运算,则必须进行另一次运算。如果您在运行时未预期方法A()的输出为null,则应引发Null指针异常,这是运行时异常。

请参考这里


2

我同意优先选择非检查异常,尤其是在设计API时。调用方始终可以选择捕获已记录的未经检查的异常。您只是不必要地强制呼叫者进行呼叫。

我发现较低的级别(作为实现细节)很有用的检查异常。与必须管理指定的错误“返回码”相比,这似乎是一种更好的控制机制。有时它也可以帮助您了解低级代码更改想法的影响...在下游声明已检查的异常,并查看谁需要进行调整。如果有很多泛型,则最后一点不适用: catch(Exception e)抛出Exception ,通常无论如何都不会考虑到这一点。


2

这是我想分享我多年的开发经验后的观点:

  1. 已检查的异常。这是业务用例或调用流程的一部分,这是我们期望或不期望的应用程序逻辑的一部分。例如,连接被拒绝,条件不满足等。我们需要处理它,并向用户显示相应的消息,并指示发生了什么以及下一步该怎么做(稍后重试等)。我通常称其为后处理异常或“用户”异常。

  2. 未经检查的异常。这是编程异常的一部分,是软件代码编程中的一些错误(错误,缺陷),反映了程序员必须如何按照文档使用API​​的方式。如果外部的lib / framework文档说它希望获取某个范围的数据且非null,因为将引发NPE或IllegalArgumentException,程序员应期望它并根据文档正确使用API​​。否则将引发异常。我通常称其为预处理异常或“验证”异常。

按目标受众。现在,让我们谈谈目标受众或人群,这些例外是经过设计的(根据我的观点):

  1. 已检查的异常。目标受众是用户/客户。
  2. 未经检查的异常。目标受众是开发人员。换句话说,未经检查的异常仅适用于开发人员。

在应用程序开发生命周期阶段。

  1. 经检查的异常被设计为在整个生产生命周期中作为应用程序处理异常情况的正常机制和预期机制而存在。
  2. 未经检查的异常被设计为仅在应用程序开发/测试生命周期中存在,所有这些异常应在该时间段内得到修复,并且当应用程序已经在生产环境中运行时不应抛出。

框架通常使用未经检查的异常(例如,Spring)的原因是,框架无法确定应用程序的业务逻辑,这取决于开发人员是否可以捕获并设计自己的逻辑。


2

我们必须根据是否是程序员错误来区分这两种类型的异常。

  • 如果错误是程序员错误,则必须是Unchecked Exception 例如:SQLException / IOException / NullPointerException。这些异常是编程错误。它们应由程序员处理。在JDBC API中,SQLException是Checked Exception,在Spring JDBCTemplate中,是Unchecked Exception。程序员在使用Spring时不必担心SqlException。
  • 如果错误不是程序员错误,并且原因来自外部,则它必须是Checked Exception。例如:如果文件被删除或其他人更改了文件许可权,则应将其恢复。

FileNotFoundException是了解细微差别的好例子。如果找不到文件,则抛出FileNotFoundException。此异常有两个原因。如果文件路径是由开发人员定义的,或者是通过GUI从最终用户那里获取的,则应为Unchecked Exception。如果文件被其他人删除,则应为“检查异常”。

Checked Exception有两种处理方式。这些正在使用try-catch或传播异常。在传播异常的情况下,由于异常处理,调用堆栈中的所有方法将紧密耦合。因此,我们必须谨慎使用Checked Exception。

如果您开发了分层的企业系统,则必须选择大多数未经检查的异常来引发,但不要忘记使用经过检查的异常以防万一。


1

对于要向调用者提供信息(例如,权限不足,找不到文件等)的可恢复情况,检查异常非常有用。

未经检查的异常很少(如果有的话)用于在运行时通知用户或程序员严重的错误或意外情况。如果您正在编写供他人使用的代码或库,请不要抛出它们,因为他们可能不希望您的软件抛出未经检查的异常,因为编译器不会强制捕获或声明它们。


我不同意您的声明“未经检查的异常很少使用,如果有的话,很少使用”,实际上这应该相反!在设计应用程序异常层次结构时,默认情况下使用未经检查的异常。让开发人员决定何时要处理异常(例如,如果他们不知道如何处理异常,则不必强迫他们放置catch块或puts子句)。
user1697575

1

如果不太可能发生异常,那么即使在捕获到异常之后我们也可以继续进行,并且我们无法做任何事情来避免该异常,那么我们可以使用已检查的异常。

每当我们希望在发生特定异常时以及在预期但不确定某个异常时做出有意义的事情时,就可以使用检查异常。

每当异常在不同的层中导航时,我们都不需要在每个层中捕获它,在这种情况下,我们可以使用运行时异常或将异常包装为未检查的异常。

当最有可能发生异常时,将使用运行时异常,它无法进行进一步处理且任何内容都无法恢复。因此,在这种情况下,我们可以针对该异常情况采取预防措施。例如:NUllPointerException,ArrayOutofBoundsException。这些更有可能发生。在这种情况下,我们可以在编码时采取预防措施,以避免此类异常。否则,我们将不得不在每个地方编写try catch块。

可以将更多常规例外设置为“未选中”,而将“较少常规”设置为“未选中”。


1

我认为我们可以从几个问题中考虑例外情况:

为什么会发生异常?当它发生时我们该怎么办

错误地,一个错误。例如一个null对象的方法被调用。

String name = null;
... // some logics
System.out.print(name.length()); // name is still null here

这种异常应在测试期间修复。否则,它将破坏生产,并且您会收到一个非常高的错误,需要立即修复。无需检查这种异常。

通过外部输入,您将无法控制或信任外部服务的输出。

String name = ExternalService.getName(); // return null
System.out.print(name.length());    // name is null here

在这里,如果您想在名称为null时继续,则可能需要检查名称是否为null,否则,可以不理会它,它将在此处停止并为调用方提供运行时异常。无需检查这种异常。

根据外部的运行时异常,您无法控制或信任外部服务。

在这里,如果要在发生外部事件时继续运行,可能需要从ExternalService捕获所有异常,否则,可以不理会它,它将在此处停止并为调用方提供运行时异常。

通过外部检查异常,您将无法控制或信任外部服务。

在这里,如果要在发生外部事件时继续运行,可能需要从ExternalService捕获所有异常,否则,可以不理会它,它将在此处停止并为调用方提供运行时异常。

在这种情况下,我们是否需要知道ExternalService中发生了哪种异常?这取决于:

  1. 如果您可以处理某些异常,则需要捕获它们并进行处理。对于其他人,将它们冒泡。

  2. 如果您需要日志或对用户特定执行的响应,则可以捕获它们。对于其他人,将它们冒泡。


0

我认为在声明应用程序异常时,它应该是未经检查的异常,即RuntimeException的子类。原因是它不会用try-catch使应用程序代码混乱,并在方法上引发声明。如果您的应用程序使用的是Java Api,它会抛出经过检查的异常,无论如何都需要处理这些异常。对于其他情况,应用程序可能会引发未经检查的异常。如果应用程序调用者仍然需要处理未检查的异常,则可以完成。


-12

我使用的规则是:永远不要使用未经检查的异常!(或者当您看不到周围的东西时)

从使用您的库的开发人员或使用您的库/应用程序的最终用户的角度来看,遇到由于意外情况而崩溃的应用程序确实很糟糕。指望全面发展也没有好处。

这样,最终用户仍然可以看到错误消息,而不是应用程序完全消失。


1
对于大多数未经检查的异常,您不会用全部解释来解释您认为的错误。
马修·

完全不同意您
毫无
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.