我阅读了一位同事的一些代码,发现他经常捕获各种异常,然后总是抛出“ RuntimeException”。我一直认为这是非常糟糕的做法。我错了吗?
Function
, Predicate
等)不具有参数化抛出条款。这意味着您需要在任何stream()方法的内部循环中捕获,包装和重新抛出所有检查的异常。这本身就为我提供了关于检查异常是否是一个坏主意的平衡建议。
我阅读了一位同事的一些代码,发现他经常捕获各种异常,然后总是抛出“ RuntimeException”。我一直认为这是非常糟糕的做法。我错了吗?
Function
, Predicate
等)不具有参数化抛出条款。这意味着您需要在任何stream()方法的内部循环中捕获,包装和重新抛出所有检查的异常。这本身就为我提供了关于检查异常是否是一个坏主意的平衡建议。
Answers:
我没有足够的背景信息来了解您的同事是否做错了什么,所以我将在一般意义上对此进行争论。
我认为将检查的异常转换为某种运行时异常并非总是不正确的做法。checked异常是经常被误用和滥用开发商。
当不打算使用检查的异常(不可恢复的条件,甚至控制流)时,使用检查的异常非常容易。尤其是如果将检查的异常用于调用者无法从中恢复的条件,我认为将其转换为带有有用消息/状态的运行时异常是合理的。不幸的是,在许多情况下,当一个人面临不可恢复的状况时,他们往往会拥有一个空的捕获块,这是您可以做的最坏的事情之一。调试此类问题是开发人员可能遇到的最大难题之一。
因此,如果您认为要处理的是可恢复条件,则应进行相应处理,并且不应将异常转化为运行时异常。如果将检查的异常用于不可恢复的条件,则将其转换为运行时异常是合理的。
它可以很好。请阅读:
http://onjava.com/pub/a/onjava/2003/11/19/exceptions.html
大多数情况下,客户端代码无法对SQLException做任何事情。不要犹豫,将它们转换为未经检查的异常。考虑以下代码:
public void dataAccessCode(){
try{
..some code that throws SQLException
}catch(SQLException ex){
ex.printStacktrace();
}
}
这个catch块只是抑制异常,什么也不做。理由是我的客户对SQLException无法做任何事情。如何以以下方式处理它?
public void dataAccessCode(){
try{
..some code that throws SQLException
}catch(SQLException ex){
throw new RuntimeException(ex);
}
}
这会将SQLException转换为RuntimeException。如果发生SQLException,则catch子句将引发新的RuntimeException。执行线程被挂起,并且异常被报告。但是,我并没有通过不必要的异常处理来破坏我的业务对象层,特别是因为它不能对SQLException做任何事情。如果我的捕获需要根本的异常原因,那么可以使用JDK1.4以来所有异常类中可用的getCause()方法。
抛出检查异常并且无法从中恢复是没有帮助的。
甚至有人认为根本不应该使用检查异常。参见 http://www.ibm.com/developerworks/java/library/j-jtp05254/index.html
最近,包括布鲁斯·埃克尔(Bruce Eckel)和罗德·约翰逊(Rod Johnson)在内的几位广受好评的专家公开表示,尽管他们最初完全同意关于检查异常的正统立场,但他们得出的结论是,仅使用检查异常并不是一个好主意。一开始就出现了,对于许多大型项目而言,检查异常已成为问题的重要根源。埃克尔(Eckel)采取了更为极端的看法,建议应不检查所有异常。约翰逊的观点较为保守,但仍表明对检查例外的正统偏好过高。(值得注意的是,几乎可以肯定的,有丰富的Java技术使用经验的C#架构师选择从语言设计中省略检查异常,使所有异常成为未检查异常。
同样来自同一链接:
使用非检查异常的决定是一个复杂的决定,很明显,没有明显的答案。Sun的建议是什么都不要使用它们,C#方法(Eckel和其他人都同意)是将它们用于所有东西。其他人则说:“有中间立场。”
不,你没看错。他的做法极为错误。您应该抛出一个异常,以捕获引起它的问题。RunTimeException的范围很广,范围很广。它应该是NullPointerException,ArgumentException等。无论准确地描述出了什么问题。这提供了区分您应处理的问题并使程序得以生存的能力,以及应属于“请勿通过”情况的错误的能力。除非问题中提供的信息中缺少某些内容,否则他的操作仅比“继续执行错误恢复”稍好。
这取决于。
这种做法可能甚至是明智的。在很多情况下(例如,在Web开发中),如果发生某些异常,您将无能为力(因为例如,您无法从代码中修复不一致的DB :-),只有开发人员才能做到。在这些情况下,明智的做法是将抛出的异常包装到运行时异常中,然后将其重新抛出。比起您可以在某些异常处理层中捕获所有这些异常,记录错误并向用户显示一些不错的本地化错误代码+消息。
另一方面,如果异常不是运行时(已检查),则API的开发人员会指出此异常是可解决的,应予以修复。如果可能,那么您绝对应该这样做。
另一个解决方案可能是将此已检查的异常重新扔到调用层,但是如果您无法解决它,则在发生异常的地方也可能无法在这里解决它...
exception handling layer
-例如在webapp中的过滤器。
我想对此发表评论,但是我发现有时候这不一定是不好的做法。(或非常糟糕)。但是也许我错了。
通常,您使用的API会引发一个异常,您无法想象在特定的用例中实际上会抛出该异常。在这种情况下,抛出RuntimeException并以捕获的异常为原因似乎是完全可以的。如果抛出此异常,则很可能是导致编程错误的原因,并且不在正确说明的范围之内。
假设稍后不会捕获并忽略RuntimeException,那么它在OnErrorResumeNext附近就没有了。
当有人捕获到异常并只是忽略它或将其打印出来时,就会发生OnErrorResumeNext。在几乎所有情况下,这都是非常糟糕的做法。
TL; DR
前提
结论
如果传播或接口代码假定基础实现依赖于外部状态,而显然不依赖于外部状态,则可以将检查后的异常作为运行时异常抛出。
本节讨论何时抛出任何一个异常的主题。如果您只想阅读结论的更详细说明,则可以跳到下一个水平条。
什么时候抛出运行时异常合适?如果很明显代码不正确,并且可以通过修改代码进行恢复,则抛出运行时异常。
例如,针对以下情况抛出运行时异常是适当的:
float nan = 1/0;
这将抛出除以零的运行时异常。这是适当的,因为代码有缺陷。
举例来说,以下HashMap
是构造函数的一部分:
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
// more irrelevant code...
}
为了固定初始容量或负载因子,您可以编辑代码以确保传入正确的值。不依赖于某些远程服务器正在运行,也不依赖于磁盘的当前状态,文件或其他程序。用无效参数调用构造函数取决于调用代码的正确性,无论是导致无效参数的错误计算还是错过错误的不正确流程。
什么时候引发检查异常?当问题可以恢复而无需更改代码时,将引发已检查的异常。或者换句话说,当错误与状态相关且代码正确时,您将引发一个已检查的异常。
现在,“恢复”一词在这里可能很棘手。这可能意味着您找到了实现该目标的另一种方法:例如,如果服务器没有响应,则应尝试下一个服务器。如果您的情况下可以进行这种恢复,那很好,但这并不是恢复的唯一方法-恢复可能只是向用户显示一个错误对话框,说明发生了什么,或者如果是服务器应用程序,则可能是向管理员发送电子邮件,甚至只是适当而简洁地记录错误。
让我们以mrmuggles的答案中提到的示例为例:
public void dataAccessCode(){
try{
..some code that throws SQLException
}catch(SQLException ex){
throw new RuntimeException(ex);
}
}
这不是处理检查的异常的正确方法。在这种方法的范围内仅不能处理异常并不意味着该应用程序应该崩溃了。相反,应该将其传播到更高的范围,如下所示:
public Data dataAccessCode() throws SQLException {
// some code that communicates with the database
}
这允许调用者进行恢复:
public void loadDataAndShowUi() {
try {
Data data = dataAccessCode();
showUiForData(data);
} catch(SQLException e) {
// Recover by showing an error alert dialog
showCantLoadDataErrorDialog();
}
}
受检查的异常是一种静态分析工具,它使程序员可以清楚地了解某个调用中可能出了什么问题,而无需他们学习实现或经过反复试验的过程。这样可以轻松确保不会忽略任何错误流。将已检查的异常重新抛出为运行时异常会不利于此节省劳动力的静态分析功能。
还值得一提的是,调用层具有更宏大的事物方案的更好的上下文,如上面所演示的。可能有许多原因dataAccessCode
被调用,该调用的特定原因仅对调用者可见-因此,它能够在失败时正确恢复时做出更好的决策。
现在我们已经清楚了这个区别,我们可以继续推断何时可以将检查后的异常重新抛出为运行时异常。
鉴于以上所述,什么时候将已检查的异常作为RuntimeException抛出是合适的?当您使用的代码假定依赖于外部状态时,您可以清楚地断言它不依赖于外部状态。
考虑以下:
StringReader sr = new StringReader("{\"test\":\"test\"}");
try {
doesSomethingWithReader(sr); // calls #read, so propagates IOException
} catch (IOException e) {
throw new IllegalStateException(e);
}
在此示例中,代码正在传播,IOException
因为的API Reader
设计为访问外部状态,但是我们知道StringReader
实现不访问外部状态。在此范围内,我们可以肯定地断言调用中涉及的部分不会访问IO或任何其他外部状态,我们可以安全地将异常作为运行时异常抛出,而不会引起不知道我们的实现的同事(并且可能假设IO访问代码将抛出IOException
)。
严格检查依赖于外部状态的异常的原因是它们是不确定的(与逻辑相关的异常不同,逻辑上的异常将每次代码版本每次都可复制)。例如,如果尝试除以0,则始终会产生异常。如果不除以0,则将永远不会产生异常,也不必处理该异常情况,因为它永远不会发生。但是,在访问文件的情况下,一次成功并不意味着您下次就可以成功-用户可能已更改权限,另一个进程可能已删除或修改了该文件。因此,您始终必须处理这种例外情况,否则您可能会遇到错误。
整个“受检查的异常”是一个坏主意。
结构化编程仅允许信息在“邻近”时在函数之间传递(或用Java的话来说是方法)。更准确地说,信息只能以两种方式在各个功能之间移动:
通过参数传递从调用者到被调用者。
从被调用方到其调用方的返回值。
这根本是一件好事。这就是让您在本地推理代码的原因:如果您需要了解或修改程序的一部分,则只需要查看该部分和其他“附近”部分即可。
但是,在某些情况下,有必要将信息发送到“远程”功能,而中间没有任何人“知道”。正是在必须使用异常的情况下。异常是从提升者(您的代码的任何部分可能包含一条语句)发送到处理程序(您的代码的任何部分可能包含与异常n 兼容的块)的秘密消息。throw
catch
throw
受检查的异常会破坏该机制的机密性,并破坏其存在的原因。如果函数可以让调用者“知道”一条信息,则只需直接将该信息作为返回值的一部分发送即可。
这可能视情况而定。在某些情况下,明智的做法是执行朋友正在做的事情,例如,当您为某些客户端公开api时,并且您希望客户端至少了解实现细节时,您知道某些实现例外可能特定于实施细节,并且不能暴露给客户。
通过避免检查的异常,您可以公开api,使客户端可以编写更简洁的代码,因为客户端本身可能正在预先验证异常条件。
例如,Integer.parseInt(String)接受一个字符串并返回它的等效整数,并在字符串不是数字的情况下引发NumberFormatException。现在,假设age
通过此方法转换了带有字段的表单提交,但是客户端已经确保了其有效性,因此没有必要强制检查异常。
这里确实有几个问题
一般的经验法则是应检查预期呼叫者会捕获并从中恢复的异常。其他例外(唯一合理的结果是中止整个操作的例外,或者您认为它们不太可能因担心专门处理它们而不值得)的例外情况应予以取消。
有时,您对异常是否值得捕获和恢复的判断与您使用的API的判断有所不同。有时上下文很重要,在一种情况下值得处理的异常可能在另一种情况下不值得处理。有时,您的手被现有接口所迫。因此,是的,有正当的理由将已检查的异常转换为未检查的异常(或其他类型的已检查异常)
首先,最重要的是,请确保使用异常链接工具。这样,原始异常中的信息就不会丢失,并且可以用于调试。
其次,您必须决定要使用哪种异常类型。使用普通的runtimeexception使调用者更难确定出了什么问题,但是如果调用者试图确定出了什么问题,则可能表明您不应该更改异常以使其不受检查。
在一个布尔型问题中,很难在两个有争议的答案后做出不同的回答,但是我想向您提供一个观点,即即使是在很少的地方提到,对于它的重要性也没有足够的强调。
这些年来,我发现总是有人对一个琐碎的问题感到困惑,他们对某些基本原理缺乏了解。
分层。一个软件应用程序(至少应该是)一堆又一堆的层。对良好分层的一个重要期望是,下层为上层中潜在的多个组件提供功能。
假设您的应用程序具有自下而上的NET,TCP,HTTP,REST,数据模型和业务的以下几层。
如果您的业务层想执行一次休息电话,请稍等。我怎么这么说 为什么不说HTTP请求或TCP事务或发送网络程序包?因为这些与我的业务层无关。我不会处理它们,也不会查看它们的细节。如果他们深陷我作为原因的异常之中,而且我不想知道它们甚至存在,我也完全可以。
而且,如果我不了解详细信息,那就不好了,因为如果明天我想更改处理特定于TCP协议的详细信息的下划线传输协议,则意味着我的REST抽象不能很好地将自身从具体的实现。
在逐层过渡异常时,重要的是重新审视异常的各个方面以及它将对当前层提供的抽象有何意义。可能是将异常替换为其他异常,也可能合并了多个异常。它还可能会将它们从选中状态转换为未选中状态,反之亦然。
当然,您提到的实际地点是一个不同的故事,但总的来说-是的,这样做可能是一件好事。
在我看来,
在框架级别,我们应该捕获运行时异常,以减少在同一位置对调用者的try catch的更多块。
在应用程序级别,我们很少捕获运行时异常,我认为这种做法是不好的。