关于java:何时选择已检查和未检查的异常

关于java:何时选择已检查和未检查的异常

When to choose checked and unchecked exceptions

在Java(或任何其他具有已检查异常的语言)中,在创建自己的异常类时,如何确定是应该选中还是取消选中它?

我的直觉是,如果调用者能够以某种富有成效的方式恢复,那么将调用一个已检查的异常,其中未经检查的异常对于不可恢复的情况更多,但我会对其他人的想法感兴趣。


只要您了解应该使用它们,检查的例外情况就很好。 Java核心API无法遵循SQLException的这些规则(有时候对于IOException),这就是它们如此糟糕的原因。

Checked Exceptions应该用于可预测但不可避免的错误,这些错误可以从中恢复。

未经检查的例外应该用于其他一切。

我会为你打破这个,因为大多数人误解了这意味着什么。

  • 可预测但不可预防:调用者尽其所能来验证输入参数,但是他们控制之外的某些条件导致操作失败。例如,您尝试读取文件,但有人在您检查文件是否存在以及读取操作开始的时间之间删除它。通过声明已检查的异常,您告诉调用者预测此失败。
  • 合理恢复:没有必要告诉呼叫者预测他们无法恢复的异常。如果用户尝试从不存在的文件中读取,则调用者可以提示他们输入新文件名。另一方面,如果方法由于编程错误(无效的方法参数或错误的方法实现)而失败,那么应用程序无法在执行中期解决问题。它能做的最好的事情是记录问题并等待开发人员稍后修复它。
  • 除非您抛出的异常符合上述所有条件,否则应使用未经检查的异常。

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

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


    来自Java学习者:

    When an exception occurs, you have to
    either catch and handle the exception,
    or tell compiler that you can't handle
    it by declaring that your method
    throws that exception, then the code
    that uses your method will have to
    handle that exception (even it also
    may choose to declare that it throws
    the exception if it can't handle it).

    Compiler will check that we have done
    one of the two things (catch, or
    declare). So these are called Checked
    exceptions. But Errors, and Runtime
    Exceptions are not checked for by
    compiler (even though you can choose
    to catch, or declare, it is not
    required). So, these two are called
    Unchecked exceptions.

    Errors are used to represent those
    conditions which occur outside the
    application, such as crash of the
    system. Runtime exceptions are
    usually occur by fault in the
    application logic. You can't do
    anything in these situations. When
    runtime exception occur, you have to
    re-write your program code. So, these
    are not checked by compiler. These
    runtime exceptions will uncover in
    development, and testing period. Then
    we have to refactor our code to remove
    these errors.


    The rule I use is: never use unchecked exceptions! (or when you don't see any way around it)

    相反的情况非常强烈:绝不使用已检查的异常。我不愿在辩论中偏袒任何一方,但似乎有一个广泛的共识,即在事后看来,引入经过检查的例外是一个错误的决定。请不要拍摄信使并参考这些论点。


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

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

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

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

    在这种情况下,API的调用者不希望捕获异常。他只想让例外"冒泡"。如果我选择了一个已检查的异常,则此调用者将有大量无用的catch块仅用于人为地重新抛出异常。

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


    你说的没错。

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

    例如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    /**
     * @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"
          .....  
     }

    只是举一个例子。关键是,如果系统快速失败,那么你就会知道它失败的地点和原因。你会得到一个堆栈跟踪:

    1
    2
    3
    4
     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"方法中的OtherClass(在第4569行)使用"exit"值调用您的类,即使它不应该等。

    否则,您将不得不在代码中进行验证,这很容易出错。此外,有时很难追踪出现问题的原因,您可能会遇到数小时令人沮丧的调试

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
     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)。


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

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

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

    对于这两个异常,我将为我的应用程序创建自己的未经检查和检查的异常(这是一个很好的实践,如此处所提到的),除了非常常见的未经检查的异常(如NullPointerException)

    因此,例如,下面这个特定函数的目标是使(或者如果已经存在)一个对象,
    含义:

    • 必须存在的对象的容器(CALLER的责任)
      =>未经检查的异常,并清除此调用函数的javadoc注释)
    • 其他参数不能为空
      (选择将编码器放在CALLER上的编码器:编码器不会检查空参数,但编码器是否有文件)
    • 结果不能为空
      (责任和被调用者代码的选择,对于调用者来说是非常感兴趣的选择
      =>检查异常,因为如果无法创建/找到对象,每个调用者必须做出决定,并且必须在编译时强制执行该决定:他们不能使用此函数而不必处理这种可能性,这意味着选中此项例外)。

    例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    /**
     * 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;
    }


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

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

    这是许多框架使用的哲学。特别是,我想到了Spring和hibernate - 它们将已知的已检查异常转换为未经检查的异常,因为在Java中过度使用了已检查的异常。我能想到的一个例子是来自json.org的JSONException,它是一个经过检查的异常,并且很烦人 - 它应该是未经检查的,但开发人员根本没有想到它。

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


    对于您的Checked / Unchecked困境,这是一个非常简单的解决方案。

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

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

    其中x为null ...
    ......代码应该有以下内容......

    1
    2
    3
    4
    5
    6
    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视为代码执行时可能出现的不可测试条件。

    1
    2
    3
    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。

    处理语言翻译时,事情变得有趣。根据上述规则,语法错误是否应被视为已检查或未检查的异常?我认为如果语言的语法可以在执行之前进行测试,那么它应该是一个UncheckedException。如果无法测试语言 - 类似于汇编代码在个人计算机上运行的方式,那么语法错误应该是一个Checked Exception。

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


    您可以将其称为已检查或未检查的异常;但是,程序员可以捕获这两种类型的异常,因此最好的答案是:将所有异常写为未选中并记录它们。这样,使用您的API的开发人员可以选择是否要捕获该异常并执行某些操作。检查异常完全浪费了每个人的时间,这使得您的代码成为令人震惊的噩梦。然后,适当的单元测试将显示您可能必须捕获并执行某些操作的任何异常。


    检查异常:
    如果客户端可以从异常中恢复并希望继续,请使用已检查的异常。

    未经检查的例外情况:
    如果客户端在异常后无法执行任何操作,则会引发未经检查的异常。

    示例:如果您希望在方法A()中进行算术运算并基于A()的输出,则必须执行另一个操作。如果输出在方法A()中为null,而在运行时期间您没有预料到,那么您应该抛出Null指针Exception,即运行时异常。

    请参考这里


    我同意对未经检查的异常的偏好,特别是在设计API时。调用者总是可以选择捕获记录的,未经检查的异常。你不是不必要地强迫来电者。

    我发现在较低级别有用的检查异常,作为实现细节。通常看起来比控制指定错误"返回代码"更好的控制机制流程。它有时可以帮助看到一个想法对低级别代码更改的影响...在下游声明一个已检查的异常并查看谁需要调整。如果有很多泛型:catch(异常e)或抛出异常,那么最后一点不适用,这通常不会过于深思熟虑。


    以下是我想分享我多年的开发经验后的看法:

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

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

  • 目标受众。现在让我们谈谈目标受众或一组例外的人(根据我的观点):

  • 检查异常。目标受众是用户/客户。
  • 未经检查的异常。目标受众是开发人员。换句话说,未经检查的异常仅为开发人员设计。
  • 按应用程序开发生命周期阶段。

  • 检查异常被设计为在整个生产生命周期中存在,作为应用程序处理特殊情况的正常和预期机制。
  • 未经检查的异常仅在应用程序开发/测试生命周期中存在,所有这些异常应在此期间修复,并且应用程序在生产中运行时不应抛出。
  • 框架通常使用未经检查的异常(例如Spring)的原因是框架无法确定应用程序的业务逻辑,这取决于开发人员是否能够捕获并设计自己的逻辑。


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

    • 如果错误是程序员错误,则必须是未经检查的异常。例如:
      的SQLException / IOException异常/ NullPointerException异常。这些例外是
      编程错误。它们应由程序员处理。而在
      JDBC API,SQLException是Checked Exception,在Spring JDBCTemplate中
      它是一个未经检查的异常。程序员不担心
      SqlException,当使用Spring时。
    • 如果错误不是程序员错误且原因来自外部,则必须是Checked Exception。例如:如果
      文件被删除或文件权限被其他人更改,它
      应该恢复。

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

    Checked Exception可以通过两种方式处理。这些是使用try-catch或传播异常。在传播异常的情况下,由于异常处理,调用堆栈中的所有方法都将紧密耦合。这就是为什么我们必须仔细使用Checked Exception。

    如果您开发了一个分层的企业系统,您必须选择大多数未经检查的异常才能抛出,但不要忘记在您无法执行任何操作的情况下使用已检查的异常。


    每当异常不太可能发生时,我们甚至可以在捕获之后继续进行,并且我们无法做任何事情来避免该异常,那么我们可以使用checked异常。

    每当我们想要在特定异常发生时做一些有意义的事情,并且当预期异常但不确定时,我们就可以使用检查异常。

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

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

    可以进行更多常规例外未选中,检查不太通用。


    我想我们可以从几个问题考虑一些问题:

    为什么会出现这种情况?当它发生时我们能做什么

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

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

    在测试期间应该修复这种异常。否则,它会破坏生产,你得到一个非常高的bug,需要立即修复。不需要检查这种异常。

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

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

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

    通过来自外部的运行时异常,您无法控制或信任外部服务。

    在这里,你可能需要捕获来自ExternalService的所有异常,如果你想在它发生时继续,否则,你可以让它独自一个,它将停在这里并给调用者一个运行时异常。

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

    在这里,你可能需要捕获来自ExternalService的所有异常,如果你想在它发生时继续,否则,你可以让它独自一个,它将停在这里并给调用者一个运行时异常。

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

  • 如果你能处理某些异常,你需要捕获它们并进行处理。对于其他人,泡他们。

  • 如果您需要记录或响应用户特定的execption,您可以捕获它们。对于其他人,泡他们。


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

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


    我认为在声明Application Exception时应该是Unchecked Exception,即RuntimeException的子类。
    原因是它不会在方法上使用try-catch和throws声明来混淆应用程序代码。如果您的应用程序使用的是Java Api,则会抛出必须处理的已检查异常。对于其他情况,应用程序可以抛出未经检查的异常。如果应用程序调用者仍需要处理未经检查的异常,则可以完成。


    我使用的规则是:永远不要使用未经检查的异常! (或当你看不到任何方式时)

    从开发人员使用您的库或最终用户使用您的库/应用程序的角度来看,真的很难面对因未知异常导致崩溃的应用程序。依靠抓住机会也无济于事。

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


    推荐阅读