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

我的直觉是,如果调用者能够以某种有效的方式恢复,则需要检查异常,而对于不可恢复的情况,则需要检查异常,但我对其他人的想法感兴趣。

有帮助吗?

解决方案

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

检查异常 应该用于 可预测, , 但 无法避免的 错误是 合理恢复.

未经检查的异常 应该用于其他一切。

我会为你解释一下,因为大多数人都误解了这意味着什么。

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

除非你抛出的异常满足 全部 对于上述情况,应该使用 Unchecked Exception。

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

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

其他提示

Java学习者:

当发生异常时,您必须捕获和处理异常,或告诉编译器您无法通过声明您的方法抛出该异常来处理它,然后使用您方法的代码必须处理该异常(即使是IT)还可以选择声明如果无法处理的话,它会引发异常)。

编译器将检查我们已经完成了两件事之一(捕获或声明)。因此,这些称为检查例外。但是,编译器未检查错误和运行时异常(即使您可以选择捕获或声明,也不需要)。因此,这两个被称为未检查的例外。

错误用于表示应用程序之外发生的情况,例如系统崩溃。运行时例外通常是通过错误的应用程序逻辑发生的。在这些情况下,您无能为力。当运行时异常时,您必须重写程序代码。因此,这些内容未由编译器检查。这些运行时例外将在开发和测试期内发现。然后,我们必须重构代码以删除这些错误。

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

有一个非常有力的证据表明相反的情况:切勿使用受检查的异常。我不愿意在辩论中选边站,但似乎有一个广泛的共识,即事后看来,引入受检异常是一个错误的决定。请不要拍信使并参考 那些 论点.

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

通过检查异常,您的错误处理策略是微管理的,并且在任何大型系统上都是难以忍受的。

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

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

在这种情况下,API 的调用者不想捕获异常。他只想让异常“冒泡”。如果我选择检查异常,则调用者将有大量无用的 catch 块,只是为了人为地重新抛出异常。

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

你是对的。

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

例如:

/**
 * @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”方法中的 OtherClass(第 4569 行)使用“exit”值调用您的类,即使它不应该等等。

否则,您将不得不在整个代码中进行验证,这很容易出错。另外,有时很难跟踪出了什么问题,您可能会花费数小时令人沮丧的调试时间

NullPointerException 也会发生同样的情况。如果您有一个 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 服务(或您之前使用的文件)时,此功能非常有用。 using 被另一个外部进程删除等)。Web 服务甚至可能在尝试连接之前进行验证,但在数据传输过程中出现了问题。

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

如果检查异常被过度使用(通过在所有方法签名中添加“抛出异常”),那么你的代码将变得非常脆弱,因为每个人都会忽略该异常(因为太笼统)并且代码质量将受到严重影响。妥协了。

如果过度使用未经检查的异常,就会发生类似的情况。该代码的用户不知道是否会出现问题,因此会出现大量 try{...}catch( Throwable t ) 。

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

  • 未经检查的异常 在我的方法的代码中 由于调用者原因导致失败 (这涉及到 明确且完整的文档)
  • 检查异常 为一个 失败是由于 被调用者 我需要向任何想要使用我的代码的人明确说明

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


对于这两个异常,我将为我的应用程序创建自己的未经检查和检查的异常(一个很好的做法, 正如这里提到的),除了非常常见的未经检查的异常(如 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;
}

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

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

这是许多框架所使用的哲学。尤其是 Spring 和 Hibernate - 它们将已知的检查异常转换为非检查异常,正是因为检查异常在 Java 中被过度使用。我能想到的一个例子是 json.org 中的 JSONException,它是一个已检查的异常,而且非常烦人 - 它应该是未检查的,但开发人员根本没有考虑清楚。

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

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

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

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

其中 x 为空......代码可能应该具有以下内容...

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,之后的任何时间,网络都可能出现故障。在读取和写入流之前,您根本无法始终测试网络。

有时,我们必须执行代码才能知道是否存在问题。通过强迫开发人员以这样的方式编写代码,迫使他们通过检查异常来处理这些情况,我必须向发明这个概念的 Java 创建者致敬。

一般来说,Java中几乎所有的API都遵循上述2条规则。如果您尝试写入文件,磁盘可能会在写入完成之前就已满。其他进程可能导致磁盘已满。根本没有办法测试这种情况。对于那些与硬件交互的人来说,使用硬件随时可能会失败,检查异常似乎是解决这个问题的一个优雅的解决方案。

这存在一个灰色地带。如果需要进行许多测试(令人兴奋的 if 语句包含大量 && 和 ||),抛出的异常将是 CheckedException,因为它太痛苦了 - 你根本不能说这个问题是一个编程错误。如果测试数量远少于 10 个(例如‘if (x == null)’),那么程序员错误应该是 UncheckedException。

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

以上两条规则可能会消除您 90% 的选择问题。要概述规则,请遵循此模式…1)如果可以在执行它正确运行之前可以对执行代码进行测试,并且如果发生异常,则如果是程序员错误,则异常应该是 UncheckedException(RuntimeException 的子类)。2)如果要执行的代码在执行之前无法进行测试才能正确运行,则该异常应该是受检查的异常(异常的子类)。

您可以将其称为已检查或未检查异常;然而, 两个都 程序员可以捕获异常类型,因此最好的答案是:写 全部 您的例外情况为 未经检查的 并记录它们。这样,使用您的 API 的开发人员就可以选择是否要捕获该异常并执行某些操作。检查异常完全是浪费每个人的时间,它让你的代码看起来像一个令人震惊的噩梦。正确的单元测试将提出您可能必须捕获并处理的任何异常。

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

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

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

参考 这里

作为一项规则,我同意对未检查异常的偏好,尤其是在设计 API 时。调用者始终可以选择捕获记录的、未经检查的异常。您只是没有不必要地强迫呼叫者这样做。

我发现检查异常在较低级别很有用,作为实现细节。它通常看起来是比必须管理指定错误“返回代码”更好的控制流机制。有时它也可以帮助了解一个想法对低级代码更改的影响......在下游声明一个已检查的异常,看看谁需要调整。如果有很多通用的,最后一点就不适用: 捕获(异常 e) 或者 抛出异常 无论如何,这通常都不是经过深思熟虑的。

下面我想分享一下我经过多年的开发经验的看法:

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

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

按目标受众。现在让我们谈谈已设计的例外情况的目标受众或人群(根据我的意见):

  1. 检查异常。目标受众是用户/客户。
  2. 未经检查的异常。目标受众是开发人员。换句话说,未经检查的异常是专为开发人员设计的。

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

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

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

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

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

当异常不太可能出现时,即使在捕获异常之后我们也可以继续,并且我们无法采取任何措施来避免该异常,那么我们可以使用检查异常。

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

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

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

可以不检查更一般的例外情况,检查不太一般的例外情况。

我认为我们可以从几个问题来思考例外:

为什么会发生异常?当它发生时我们能做什么

错误地,一个错误。 比如调用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

这里,如果 name 为 null 时你想继续,你可能需要检查 name 是否为 null,否则,你可以不管它,它会停在这里并给调用者提供运行时异常。这种异常不需要检查。

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

在这里,如果你想在发生时继续,你可能需要捕获来自ExternalService的所有异常,否则,你可以不管它,它会停在这里并向调用者提供运行时异常。

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

在这里,如果你想在发生时继续,你可能需要捕获来自ExternalService的所有异常,否则,你可以不管它,它会停在这里并向调用者提供运行时异常。

那么,我们是否需要知道ExternalService中发生了什么样的异常呢? 这取决于:

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

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

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

  • 如果错误是程序员错误,那么它一定是 Unchecked Exception. 例如:SQLException/IOException/NullPointerException。这些例外是编程错误。它们应该由程序员处理。在JDBC API中,检查了SQLEXCEPTION例外,在Spring JDBCtemplate中,这是一个未检查的例外。程序员不必担心SQLEXCEPTION,当时使用Spring时。
  • 如果错误不是程序员错误并且原因来自外部,那么它一定是受检查的异常。 例如:如果文件已删除或其他人更改文件权限,则应将其恢复。

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

检查异常可以通过两种方式处理。这些使用 try-catch 或传播异常。如果发生异常,调用堆栈中的所有方法都将被 紧密耦合 因为异常处理。这就是为什么我们必须谨慎使用 Checked Exception。

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

我认为在声明应用程序异常时,它应该是未经检查的异常,即 RuntimeException 的子类。原因是它不会因方法上的 try-catch 和 throws 声明而使应用程序代码变得混乱。如果您的应用程序使用 Java Api,它会抛出无论如何都需要处理的已检查异常。对于其他情况,应用程序可以抛出未经检查的异常。如果应用程序调用者仍然需要处理未检查的异常,那么可以这样做。

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

从使用您的库的开发人员或使用您的库/应用程序的最终用户的角度来看,遇到由于不可预见的异常而崩溃的应用程序确实很糟糕。指望包罗万象也不好。

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

许可以下: CC-BY-SA归因
不隶属于 StackOverflow
scroll top