在 Java(或任何其他具有检查异常的语言)中,当创建自己的异常类时,您如何决定是否应该检查或不检查?
我的直觉是,在调用者可能能够以某种有效的方式恢复的情况下,会调用检查异常,而对于不可恢复的情况,未经检查的异常会更多,但我会对其他人的想法感兴趣。
在 Java(或任何其他具有检查异常的语言)中,当创建自己的异常类时,您如何决定是否应该检查或不检查?
我的直觉是,在调用者可能能够以某种有效的方式恢复的情况下,会调用检查异常,而对于不可恢复的情况,未经检查的异常会更多,但我会对其他人的想法感兴趣。
Checked Exceptions 很棒,只要你明白什么时候应该使用它们。Java 核心 API 未能遵循 SQLException(有时是 IOException)的这些规则,这就是它们如此糟糕的原因。
Checked Exceptions应该用于可预测但无法预防的错误,这些错误可以合理地从中恢复。
未经检查的异常应该用于其他一切。
我会为你分解这个,因为大多数人误解了这意味着什么。
除非您抛出的异常满足上述所有条件,否则它应该使用未经检查的异常。
在每个级别重新评估:有时捕获检查异常的方法不是处理错误的正确位置。在这种情况下,请考虑对您自己的调用者来说什么是合理的。如果异常是可预测的、不可预防的并且可以合理地让他们从中恢复,那么您应该自己抛出一个已检查的异常。如果不是,您应该将异常包装在未经检查的异常中。如果您遵循此规则,您会发现自己将已检查的异常转换为未检查的异常,反之亦然,具体取决于您所在的层。
对于已检查和未检查的异常,请使用正确的抽象级别。例如,具有两种不同实现(数据库和文件系统)的代码存储库应避免通过抛出SQLException
或IOException
. 相反,它应该将异常包装在跨越所有实现的抽象中(例如RepositoryException
)。
来自Java 学习者:
当发生异常时,您必须捕获并处理异常,或者通过声明您的方法抛出该异常来告诉编译器您无法处理它,然后使用您的方法的代码将不得不处理该异常(即使它如果不能处理,也可以选择声明抛出异常)。
编译器将检查我们是否完成了两件事之一(捕获或声明)。所以这些被称为检查异常。但是编译器不会检查错误和运行时异常(即使您可以选择捕获或声明,这也不是必需的)。因此,这两个被称为 Unchecked 异常。
错误用于表示在应用程序之外发生的那些情况,例如系统崩溃。运行时异常通常是由于应用程序逻辑中的错误而发生的。在这些情况下,您无能为力。当运行时异常发生时,您必须重新编写程序代码。因此,编译器不会检查这些。这些运行时异常将在开发和测试期间发现。然后我们必须重构我们的代码来消除这些错误。
在任何足够大、有很多层的系统上,检查的异常都是无用的,因为无论如何,您需要一个架构级别的策略来处理异常的处理方式(使用故障屏障)
通过检查异常,您的错误处理策略是微管理的,并且在任何大型系统上都无法忍受。
大多数时候,您不知道错误是否“可恢复”,因为您不知道 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”方法(第 4569 行)中的 OtherClass 使用“exit”值调用您的类,即使它不应该等等。
否则,您将不得不在整个代码中进行验证,这很容易出错。另外,有时很难跟踪出了什么问题,您可能会期待数小时的令人沮丧的调试
NullPointerExceptions 也会发生同样的事情。如果您有一个包含 15 个方法的 700 行类,它使用 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
....
}
Checked exceptions当程序员(你或你的同事)做对了所有事情,验证了输入,运行了测试,并且所有的代码都是完美的,但是代码连接到第三方 web 服务可能已经关闭(或文件)时很有用您正在使用的已被另一个外部进程等删除)。Web 服务甚至可以在尝试连接之前进行验证,但在数据传输过程中出现了问题。
在这种情况下,您或您的同事无能为力。但是您仍然必须做一些事情,而不是让应用程序在用户眼中死去并消失。您为此使用检查的异常并处理异常,当发生这种情况时您能做什么?,大多数时候,只是为了尝试记录错误,可能会保存您的工作(应用程序工作)并向用户显示消息. (网站blabla已关闭,请稍后重试等)
如果检查的异常被过度使用(通过在所有方法签名中添加“抛出异常”),那么您的代码将变得非常脆弱,因为每个人都会忽略该异常(因为太笼统)并且代码质量会很严重妥协。
如果你过度使用未经检查的异常,就会发生类似的事情。该代码的用户不知道是否会出现问题,因此会出现很多 try{...}catch( Throwable t ) 。
这是我的“最终经验法则”。
我用:
与上一个答案相比,这是使用一种或另一种(或两种)例外的明确理由(可以同意或不同意)。
对于这两个异常,我将为我的应用程序创建自己的未检查和检查异常(一个很好的做法,如此处所述),除了非常常见的未检查异常(如 NullPointerException)
例如,下面这个特定函数的目标是创建(或者如果已经存在)一个对象,
意思是:
例子:
/**
* 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:将未经检查的异常视为代码执行之前的可测试条件。比如……</p>
x.doSomething(); // the code throws a NullPointerException
其中 x 为空... ...代码可能应具有以下内容...</p>
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。
与语言翻译打交道时,事情变得有趣起来。根据上面的规则,语法错误应该被认为是受检异常还是未受检异常?我会争辩说,如果可以在执行该语言的语法之前对其进行测试,那么它应该是 UncheckedException。如果语言无法测试——类似于汇编代码在个人计算机上的运行方式,那么语法错误应该是一个检查异常。
以上 2 条规则可能会消除 90% 的选择问题。总结规则,遵循这个模式…… 1) 如果要执行的代码可以在执行之前进行测试以使其正确运行,并且如果发生异常——也就是程序员错误,则异常应该是 UncheckedException(RuntimeException 的子类)。2)如果要执行的代码在执行之前无法测试以使其正确运行,则异常应该是Checked Exception(Exception的子类)。
您可以将其称为已检查或未检查的异常;但是,程序员可以捕获这两种类型的异常,因此最好的答案是:将所有异常写为未检查并记录它们。这样,使用您的 API 的开发人员可以选择是否要捕获该异常并执行某些操作。检查异常完全浪费了每个人的时间,它让你的代码看起来令人震惊。然后,适当的单元测试将提出您可能必须捕获并处理的任何异常。
检查异常: 如果客户端可以从异常中恢复并希望继续,请使用检查异常。
未经检查的异常: 如果客户端在异常之后不能做任何事情,则引发未经检查的异常。
示例:如果您希望在方法 A() 中进行算术运算并基于 A() 的输出,则必须进行另一个运算。如果在运行时您不期望方法 A() 的输出为空,那么您应该抛出空指针异常,这是运行时异常。
参考这里
以下是我想分享我多年开发经验后的看法:
检查异常。这是业务用例或调用流程的一部分,这是我们期望或不期望的应用程序逻辑的一部分。例如连接被拒绝,条件不满足等。我们需要处理它并向用户显示相应的消息,说明发生了什么以及下一步做什么(稍后再试等)。我通常称之为后处理异常或“用户”异常。
未经检查的异常。这是编程异常的一部分,是软件代码编程中的一些错误(bug、缺陷),反映了程序员必须按照文档使用 API 的方式。如果外部 lib/framework 文档说它希望获取某个范围内的非 null 数据,因为 NPE 或 IllegalArgumentException 将被抛出,程序员应该期待它并按照文档正确使用 API。否则会抛出异常。我通常称之为预处理异常或“验证”异常。
按目标受众。现在让我们谈谈设计了例外的目标受众或人群(根据我的观点):
按应用程序开发生命周期阶段。
框架通常使用未经检查的异常(例如 Spring)的原因是框架无法确定您的应用程序的业务逻辑,这取决于开发人员捕获然后设计自己的逻辑。
作为一项规则,我同意对未经检查的异常的偏好,尤其是在设计 API 时。调用者始终可以选择捕获已记录的、未经检查的异常。你只是没有不必要地强迫呼叫者。
我发现检查的异常在较低级别很有用,作为实现细节。与必须管理指定的错误“返回代码”相比,它通常似乎是一种更好的控制机制。它有时也可以帮助查看低级代码更改的想法的影响......在下游声明一个检查异常,看看谁需要调整。如果有很多泛型: catch(Exception e) 或throws Exception ,则最后一点不适用,这通常不是经过深思熟虑的。
我们必须根据是否是程序员错误来区分这两种类型的异常。
FileNotFoundException 是理解细微差别的好例子。如果找不到文件,则会引发 FileNotFoundException。这个例外有两个原因。如果文件路径由开发人员定义或通过 GUI 从最终用户获取,则它应该是未经检查的异常。如果文件被其他人删除,则应为 Checked Exception。
Checked Exception 可以通过两种方式处理。这些正在使用 try-catch 或传播异常。在异常传播的情况下,调用堆栈中的所有方法将因为异常处理而紧密耦合。这就是为什么,我们必须谨慎使用 Checked Exception。
如果你开发一个分层的企业系统,你必须选择大部分未检查的异常来抛出,但不要忘记在你不能做任何事情的情况下使用检查的异常。
检查的异常对于您想向调用者提供信息的可恢复情况很有用(即权限不足、找不到文件等)。
未经检查的异常很少(如果有的话)用于在运行时通知用户或程序员严重错误或意外情况。如果您正在编写将被其他人使用的代码或库,请不要抛出它们,因为他们可能不希望您的软件抛出未经检查的异常,因为编译器不会强制它们被捕获或声明。
当异常不太可能发生时,即使在捕获该异常之后我们也可以继续,并且我们无法做任何事情来避免该异常,那么我们可以使用已检查异常。
每当我们想要在发生特定异常时以及当该异常是预期但不确定时做一些有意义的事情时,我们就可以使用受检异常。
每当在不同层中导航异常时,我们不需要在每一层都捕获它,在这种情况下,我们可以使用运行时异常或将异常包装为未经检查的异常。
运行时异常用于最有可能发生异常、无法进一步处理且无法恢复的情况。因此,在这种情况下,我们可以针对该异常采取预防措施。例如:NUllPointerException、ArrayOutofBoundsException。这些更有可能发生。在这种情况下,我们可以在编码时采取预防措施来避免此类异常。否则,我们将不得不在每个地方编写 try catch 块。
更一般的例外可以被取消检查,不太一般的被检查。
我认为我们可以考虑几个问题的例外:
为什么会发生例外?当它发生时我们能做什么
错误地,一个错误。比如调用空对象的方法。
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
在这里,如果你想在它为空时继续,你可能需要检查名称是否为空,否则你可以不管它,它会停在这里并给调用者运行时异常。这种异常不需要检查。
由于来自外部的运行时异常,您无法控制或信任外部服务。
在这里,如果你想在 ExternalService 发生异常时继续,你可能需要从 ExternalService 捕获所有异常,否则,你可以不管它,它会停在这里并给调用者运行时异常。
通过外部检查异常,您无法控制或信任外部服务。
在这里,如果你想在 ExternalService 发生异常时继续,你可能需要从 ExternalService 捕获所有异常,否则,你可以不管它,它会停在这里并给调用者运行时异常。
在这种情况下,我们是否需要知道 ExternalService 发生了什么样的异常?这取决于:
如果您可以处理某些类型的异常,则需要捕获它们并进行处理。对于其他人,给他们起泡。
如果您需要记录或响应用户特定的执行,您可以捕获它们。对于其他人,给他们起泡。
我认为在声明应用程序异常时,它应该是未经检查的异常,即 RuntimeException 的子类。原因是它不会使用 try-catch 和 throws 方法声明使应用程序代码混乱。如果您的应用程序正在使用 Java Api,它会抛出无论如何都需要处理的已检查异常。对于其他情况,应用程序可以抛出未经检查的异常。如果应用程序调用者仍需要处理未经检查的异常,则可以完成。
我使用的规则是:永远不要使用未经检查的异常!(或者当你看不到任何方法时)
从使用您的库的开发人员或使用您的库/应用程序的最终用户的角度来看,面对一个由于意外异常而崩溃的应用程序真的很糟糕。并且指望包罗万象也不好。
这样,最终用户仍然可以看到错误消息,而不是应用程序完全消失。