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

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

我的直觉是,在调用者可能能够以某种有效的方式恢复的情况下,将调用checked异常,而作为未检查的异常则更多地用于不可恢复的情况,但我对其他人的想法感兴趣。

66792 次浏览

Java学习者:

当异常发生时,你必须 要么捕获并处理异常, 或者告诉编译器你不能处理 通过声明你的方法 抛出该异常,然后抛出代码 用你的方法就得 处理该异常(即使它也 可以选择申报它投吗 异常如果它不能处理它)。< / p > 编译器将检查我们已经完成的操作 两件事之一(抓,或 声明)。这些叫做Checked 例外。但是错误和运行时 不检查异常 编译器(即使你可以选择 捕获,或宣布,它不是 需要)。这两个叫做 未经检查的异常。< / p >

错误用于表示这些错误 发生在 应用程序,如崩溃的 系统。运行时异常包括 通常发生在故障中 应用程序逻辑。你不能这样做 在这种情况下。当 运行时异常发生时,你不得不这样做 重写你的程序代码。因此,这些 编译器不检查。这些 运行时异常将在 开发、测试阶段。然后 我们必须重构代码来移除 这些错误。< / p >

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

从使用您的库的开发人员或使用您的库/应用程序的最终用户的角度来看,遇到由于不应出现的异常而导致应用程序崩溃的情况真的很糟糕。指望包罗万象也不行。

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

当你想要向调用者提供信息时,受控异常对于可恢复的情况很有用(例如,权限不足,文件未找到等)。

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

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

有一个相反的情况:从来没有使用受控异常。我不愿意在辩论中偏袒任何一方(双方都有很好的论据!),但相当多的专家认为,事后看来,受控例外是一个错误的决定。

有关讨论,请查看WikiWikiWeb的“受控异常的价值值得怀疑”。早期扩展参数的另一个例子是Rod Waldhoff的博客文章

这是我的“最终规则” 我使用:< / p >

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

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


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

例如,下面这个特定函数的目标是创建(或获取)一个对象,
意义:< / p >
  • make/get对象的容器必须存在(CALLER的责任
    =>未检查的异常,并清除此被调用函数的javadoc注释)
  • 其他参数不能为空
    (选择的编码器把它放在CALLER:编码器将不检查空参数,但编码器文档IT)
  • 结果不能为NULL
    (被调用方代码的责任和选择,调用方将非常感兴趣的选择
    => checked异常,因为每个调用者必须在无法创建/找到对象时做出决定,并且该决定必须在编译时强制执行:如果不处理这种可能性,就不能使用此函数,这意味着存在检查异常)。李< / >

例子:


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

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

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

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

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

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

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

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

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

这是许多框架使用的哲学。尤其是Spring和hibernate——它们精确地将已知的受控异常转换为未检查异常,因为Java中过度使用了受控异常。我能想到的一个例子是来自json.org的JSONException,这是一个受检查的异常,最烦人的是——它应该是不受检查的,但开发人员根本没有考虑到这一点。

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

你是正确的。

未经检查的异常是用来让系统快速失败,这是一件好事。为了正常工作,你应该清楚地说明你的方法期望什么。通过这种方式,您可以只验证输入一次。

例如:

/**
* @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个属性,它们都不能为空,而不是在每个方法中验证可为空性,你可以使所有这些属性都是只读的,并在构造函数或工厂方法中验证它们。

 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”),那么您的代码将变得非常脆弱,因为每个人都会忽略这个异常(因为太普遍),代码质量将严重受损。

如果过度使用未检查异常,也会发生类似的情况。这段代码的用户不知道是否会出现错误,因此进行了大量的尝试{…}catch(Throwable t)将出现。

您可以将其称为已检查异常或未检查异常;然而,这两个类型的异常可以被程序员捕获,所以最好的答案是:将异常的所有写成无节制的并记录它们。这样,使用你的API的开发人员就可以选择他或她是否想要捕获这个异常并做一些事情。受控异常完全是在浪费每个人的时间,它会让你的代码看起来像一个可怕的噩梦。然后,适当的单元测试将显示您可能必须捕获并处理的任何异常。

< >强检查例外: 如果客户端可以从异常中恢复并希望继续,则使用checked exception.

< >强不例外: 如果客户端在异常发生后不能做任何事情,则引发未检查的异常

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

参考在这里

我同意将未检查异常作为规则的偏好,特别是在设计API时。调用方总是可以选择捕获记录在案的、未检查的异常。你只是没有必要强迫打电话的人。

我发现作为实现细节,受控异常在较低级别很有用。与必须管理指定的错误“返回码”相比,这似乎是一种更好的控制机制流。它有时也可以帮助看到一个想法对低级代码更改的影响……在下游声明一个检查过的异常,看看谁需要进行调整。最后一点不适用,如果有很多泛型:捕获(异常e)抛出异常,这通常不是太周到的考虑。

受控异常非常好,只要你知道什么时候应该使用它们。对于SQLException(有时对于IOException), Java核心API无法遵循这些规则,这就是它们如此糟糕的原因。

已检查的异常应该用于可预测的,但无法预防的错误是合理地从

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

我来解释一下,因为大多数人都误解了这句话的意思。

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

除非你抛出的异常符合上述条件中的所有,否则它应该使用未检查异常。

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

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

当不太可能出现异常时,即使在捕捉到异常之后,我们也可以继续,并且我们不能做任何事情来避免该异常,那么我们可以使用受控异常。

当我们想做一些有意义的事情时,当一个特定的异常发生时,当这个异常是预期的,但不是确定的,那么我们可以使用受控异常。

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

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

更一般的例外情况可以设置为Unchecked,不太一般的例外情况将被检查。

这里有一个非常简单的方法来解决你的“受控/未受控”困境。

规则1:将未检查异常视为代码执行前的可测试条件。 例如…< / p >
x.doSomething(); // the code throws a NullPointerException

where x is 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();

在上面的例子中,URL (google.com)可能由于DNS服务器宕机而不可用。即使在DNS服务器工作并将“google.com”名称解析为IP地址的瞬间,如果连接到google.com,在任何时候,网络都可能瘫痪。你不能在读写流之前一直测试网络。

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

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

这是一个灰色地带。在需要许多测试的情况下(使用大量&&和||),抛出的异常将是一个CheckedException,因为它太痛苦了,你不能简单地说这个问题是一个编程错误。如果测试少于10个(例如' If (x == null) '),那么程序员错误应该是UncheckedException。

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

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

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

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

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

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

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

通过外部输入,你不能控制或信任外部服务的输出。

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

在这里,如果你想在它为空时继续,你可能需要检查名称是否为空,否则,你可以让它在这里停止,并给调用者运行时异常。 这类异常不需要检查

通过运行时异常从外部,你不能控制或信任外部服务。

在这里,如果您希望在发生异常时继续执行,则可能需要从ExternalService捕获所有异常,否则,您可以不去管它,它将在这里停止并向调用者提供运行时异常。

通过外部检查异常,你不能控制或信任外部服务。

在这里,如果您希望在发生异常时继续执行,则可能需要从ExternalService捕获所有异常,否则,您可以不去管它,它将在这里停止并向调用者提供运行时异常。

这取决于:

  1. 如果您可以处理某些类型的异常,则需要捕获它们并进行处理。对其他人来说,泡一下。

  2. 如果您需要记录或响应用户特定的异常,您可以捕获它们。对其他人来说,泡一下。

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

以下是我在多年开发经验后的一些看法:

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

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

目标受众。现在让我们来谈谈目标受众或设计例外的人群(根据我的观点):

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

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

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

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

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

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

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

检查异常可以用两种方式处理。它们使用try-catch或传播异常。在异常传播的情况下,由于异常处理,调用堆栈中的所有方法都将是紧耦合的。这就是为什么我们必须小心地使用已检查异常。

如果您开发了一个分层的企业系统,您必须选择大多数未检查的异常来抛出,但不要忘记在您什么都做不了的情况下使用已检查的异常。