捕获已检查的异常并引发RuntimeException是一种好习惯吗?


70

我阅读了一位同事的一些代码,发现他经常捕获各种异常,然后总是抛出“ RuntimeException”。我一直认为这是非常糟糕的做法。我错了吗?


17
“检查的异常的价格违反了开放式/封闭式原则。如果您从代码中的某个方法抛出了一个检查的异常,并且捕获量位于以上三个级别,则必须在您和捕获之间的每个方法的签名中声明该异常。这意味着在软件的较低级别上进行更改可能会在许多较高级别上强制进行签名更改。” —罗伯特·马丁(Robert C. Martin),《清洁代码》,第107页
桑戈,2014年

6
有趣的是,Jim Waldo在“ Java:The Good Parts” shop.oreilly.com/product/9780596803742.do中声明了未经检查的异常,称成年程序员只应抛出检查的异常。仅在6年前,我们在JUG中阅读了它,这似乎是一个很好的建议!现在,通过函数式编程,已检查的异常完全变得难以处理。像Scala和Kotlin这样的语言甚至都没有。我也已经开始包装未检查的异常。
GlenPeterson

@GlenPeterson您还可以在FP中获得建议,以完全避免执行,而应使用总和类型
jk。

还有的功能接口的明显的例子:内置的功能接口(即FunctionPredicate等)不具有参数化抛出条款。这意味着您需要在任何stream()方法的内部循环中捕获,包装和重新抛出所有检查的异常。这本身就为我提供了关于检查异常是否是一个坏主意的平衡建议。
Joel Cornett

创建RuntimeException的自定义子类以通过您的异常传达含义没有错。
Joel Cornett

Answers:


55

我没有足够的背景信息来了解您的同事是否做错了什么,所以我将在一般意义上对此进行争论。

我认为将检查的异常转换为某种运行时异常并非总是不正确的做法checked异常经常被误用滥用开发商。

当不打算使用检查的异常(不可恢复的条件,甚至控制流)时,使用检查的异常非常容易。尤其是如果将检查的异常用于调用者无法从中恢复的条件,我认为将其转换为带有有用消息/状态的运行时异常是合理的。不幸的是,在许多情况下,当一个人面临不可恢复的状况时,他们往往会拥有一个空的捕获块,这是您可以做的最坏的事情之一。调试此类问题是开发人员可能遇到的最大难题之一。

因此,如果您认为要处理的是可恢复条件,则应进行相应处理,并且不应将异常转化为运行时异常。如果将检查的异常用于不可恢复的条件,则将其转换为运行时异常是合理的


17
在大多数实际应用中,很少有无法恢复的情况。您几乎可以在某个级别上说:“好,此操作失败,因此我们显示/记录一个不错的错误消息,然后继续/等待下一个。”
Michael Borgwardt

6
确实如此,@ MichaelBorgwardt,但是这类处理的位置通常是在应用程序的最高级别,因此,每当我看到开发人员在较低级别“处理”异常时,通常很容易删除他们的处理并渗透异常向上。例如,像JSF这样的Web框架在最高级别捕获异常,打印日志消息,并继续处理其他请求(并不是说默认处理是合适的,仅是示例)。
DavidS

40

可以很好。请阅读:

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和其他人都同意)是将它们用于所有东西。其他人则说:“有中间立场。”


13

不,你没看错。他的做法极为错误。您应该抛出一个异常,以捕获引起它的问题。RunTimeException的范围很广,范围很广。它应该是NullPointerException,ArgumentException等。无论准确地描述出了什么问题。这提供了区分您应处理的问题并使程序得以生存的能力,以及应属于“请勿通过”情况的错误的能力。除非问题中提供的信息中缺少某些内容,否则他的操作仅比“继续执行错误恢复”稍好。


1
感谢您的提示。如果他抛出一个已实现的自定义异常,该异常直接继承自RuntimeException怎么办?
RoflcoptrException 2011年

27
@Gary Buyn:很多人认为检查型异常是一个失败的语言设计实验,他们是应该尽量少用,而不是习惯的问题的人。
Michael Borgwardt

7
@Gary Buyn:以下文章很好地概述了辩论:ibm.com/developerworks/java/library/j-jtp05254/index.html还要注意,在Java引入此功能15年后,没有其他语言采用过该功能, C ++不赞成使用类似功能。
Michael Borgwardt

7
@c_maker:实际上,Bloch主要提倡正统观点,您的评论似乎主要是关于使用更多的例外,句点。我的观点是,使用检查异常的唯一有效原因是希望所有调用方都能立即处理的情况。
Michael Borgwardt

14
不必要的引发检查异常会违反封装。如果像“ getAccounts()”这样的简单方法向您抛出“ SQLException”,“ NullPointerException”或“ FileNotFoundException”,您该怎么办?你能应付吗?您可能只是“抓住(异常e){}”而已。此外,这些例外-具体实现!它不应该是合同的一部分!您只需要知道有一个错误。如果实施发生变化怎么办?突然所有事情都必须更改,导致该方法不再引发“ SQLException”,而是引发“ ParseXMLException”!
KL 2012年

8

这取决于。

这种做法可能甚至是明智的。在很多情况下(例如,在Web开发中),如果发生某些异常,您将无能为力(因为例如,您无法从代码中修复不一致的DB :-),只有开发人员才能做到。在这些情况下,明智的做法是将抛出的异常包装到运行时异常中,然后将其重新抛出。比起您可以在某些异常处理层中捕获所有这些异常,记录错误并向用户显示一些不错的本地化错误代码+消息。

另一方面,如果异常不是运行时(已检查),则API的开发人员会指出此异常是可解决的,应予以修复。如果可能,那么您绝对应该这样做。

另一个解决方案可能是将此已检查的异常重新扔到调用层,但是如果您无法解决它,则在发生异常的地方也可能无法在这里解决它...


您希望API的开发人员知道他/她在做什么,并且很好地使用了检查异常。我开始看到倾向于抛出运行时异常并同时对其进行记录的API,因此客户端可以选择捕获它。
c_maker 2011年

引发异常的方法通常无法知道调用者是否可以从中恢复。另一方面,我建议如果知道内部方法为何引发异常,并且原因与外部方法的API一致,则仅应让内部方法抛出的已检查异常逃逸。如果内部方法意外引发了检查异常,则将其冒泡作为检查异常可能会使调用者误解所发生的情况。
2014年

2
感谢您提及exception handling layer-例如在webapp中的过滤器。
杰克·多伦多

5

我想对此发表评论,但是我发现有时候这不一定是不好的做法。(或非常糟糕)。但是也许我错了。

通常,您使用的API会引发一个异常,您无法想象在特定的用例中实际上会抛出该异常。在这种情况下,抛出RuntimeException并以捕获的异常为原因似乎是完全可以的。如果抛出此异常,则很可能是导致编程错误的原因,并且不在正确说明的范围之内。

假设稍后不会捕获并忽略RuntimeException,那么它在OnErrorResumeNext附近就没有了。

当有人捕获到异常并只是忽略它或将其打印出来时,就会发生OnErrorResumeNext。在几乎所有情况下,这都是非常糟糕的做法。


这可能是在调用树顶部附近的情况,您唯一可以做的就是尝试正常恢复,并且知道特定的错误实际上并没有帮助。在这种情况下,您可能必须记录错误并继续操作(处理下一条记录,通知用户发生错误等)。否则,不会。您应该始终在尽可能接近错误的情况下处理异常,而不是将其包装为下一个处理程序的白象。
Michael K

@MichaelK问题实际上是“尽可能接近错误”,这通常意味着“通过几层无法直接控制的中间层”。例如,如果我的班级必须实现某个接口,那么我的双手会被束缚。这可以在调用树中任意深处发生。即使接口在我的控制之下,如果可以想象的只有有限的一组具体实现,添加throw声明也会使抽象性泄漏。让每个客户为一些实施细节付钱并不是一个伟大的设计折衷。
蒂姆·塞吉因

4

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,则将永远不会产生异常,也不必处理该异常情况,因为它永远不会发生。但是,在访问文件的情况下,一次成功并不意味着您下次就可以成功-用户可能已更改权限,另一个进程可能已删除或修改了该文件。因此,您始终必须处理这种例外情况,否则您可能会遇到错误。



2

这是许多框架中的普遍做法。例如Hibernate,正是这样做的。想法是,API不应对客户端Exception具有干扰性,而应具有侵入性,因为您必须在调用api的地方显式编写代码来处理它们。但是该位置可能不是首先处理异常的正确位置。
实话实说,这是一个“热门”话题,并且有很多争议,因此我不会支持,但我要说的是,您的朋友所做的/提出的建议并非不寻常或不寻常。


1

整个“受检查的异常”是一个坏主意。

结构化编程仅允许信息在“邻近”时在函数之间传递(或用Java的话来说是方法)。更准确地说,信息只能以两种方式在各个功能之间移动:

  1. 通过参数传递从调用者到被调用者。

  2. 从被调用方到其调用方的返回值。

这根本是一件好事。这就是让您在本地推理代码的原因:如果您需要了解或修改程序的一部分,则只需要查看该部分和其他“附近”部分即可。

但是,在某些情况下,有必要将信息发送到“远程”功能,而中间没有任何人“知道”。正是在必须使用异常的情况下。异常是从提升者(您的代码的任何部分可能包含一条语句)发送到处理程序(您的代码的任何部分可能包含与异常n 兼容的块)的秘密消息。throwcatchthrow

受检查的异常会破坏该机制的机密性,并破坏其存在的原因。如果函数可以让调用者“知道”一条信息,则只需直接将该信息作为返回值的一部分发送即可。


值得一提的是,在方法运行由其调用者提供的功能的情况下,此类问题确实会造成严重破坏。接收该函数的方法的作者在许多情况下将没有理由知道或关心调用者期望它执行的操作,也没有任何理由调用者可能期望的异常。如果接收到该方法的代码不希望它引发已检查的异常,则所提供的方法可能必须包装它将抛出的所有已检查的异常包装在供供应商随后捕获的未检查的异常中。
超级猫

0

这可能视情况而定。在某些情况下,明智的做法是执行朋友正在做的事情,例如,当您为某些客户端公开api时,并且您希望客户端至少了解实现细节时,您知道某些实现例外可能特定于实施细节,并且不能暴露给客户。

通过避免检查的异常,您可以公开api,使客户端可以编写更简洁的代码,因为客户端本身可能正在预先验证异常条件。

例如,Integer.parseInt(String)接受一个字符串并返回它的等效整数,并在字符串不是数字的情况下引发NumberFormatException。现在,假设age通过此方法转换了带有字段的表单提交,但是客户端已经确保了其有效性,因此没有必要强制检查异常。


0

这里确实有几个问题

  1. 您是否应该将检查的异常转换为未检查的异常?

一般的经验法则是应检查预期呼叫者会捕获并从中恢复的异常。其他例外(唯一合理的结果是中止整个操作的例外,或者您认为它们不太可能因担心专门处理它们而不值得)的例外情况应予以取消。

有时,您对异常是否值得捕获和恢复的判断与您使用的API的判断有所不同。有时上下文很重要,在一种情况下值得处理的异常可能在另一种情况下不值得处理。有时,您的手被现有接口所迫。因此,是的,有正当的理由将已检查的异常转换为未检查的异常(或其他类型的已检查异常)

  1. 如果要将非检查型异常转换为检查型异常,应该怎么做。

首先,最重要的是,请确保使用异常链接工具。这样,原始异常中的信息就不会丢失,并且可以用于调试。

其次,您必须决定要使用哪种异常类型。使用普通的runtimeexception使调用者更难确定出了什么问题,但是如果调用者试图确定出了什么问题,则可能表明您不应该更改异常以使其不受检查。


0

在一个布尔型问题中,很难在两个有争议的答案后做出不同的回答,但是我想向您提供一个观点,即即使是在很少的地方提到,对于它的重要性也没有足够的强调。

这些年来,我发现总是有人对一个琐碎的问题感到困惑,他们对某些基本原理缺乏了解。

分层。一个软件应用程序(至少应该是)一堆又一堆的层。对良好分层的一个重要期望是,下层为上层中潜在的多个组件提供功能。

假设您的应用程序具有自下而上的NET,TCP,HTTP,REST,数据模型和业务的以下几层。

如果您的业务层想执行一次休息电话,请稍等。我怎么这么说 为什么不说HTTP请求或TCP事务或发送网络程序包?因为这些与我的业务层无关。我不会处理它们,也不会查看它们的细节。如果他们深陷我作为原因的异常之中,而且我不想知道它们甚至存在,我也完全可以。

而且,如果我不了解详细信息,那就不好了,因为如果明天我想更改处理特定于TCP协议的详细信息的下划线传输协议,则意味着我的REST抽象不能很好地将自身从具体的实现。

在逐层过渡异常时,重要的是重新审视异常的各个方面以及它将对当前层提供的抽象有何意义。可能是将异常替换为其他异常,也可能合并了多个异常。它还可能会将它们从选中状态转换为未选中状态,反之亦然。

当然,您提到的实际地点是一个不同的故事,但总的来说-是的,这样做可能是一件好事。


-2

在我看来,

在框架级别,我们应该捕获运行时异常,以减少在同一位置对调用者的try catch的更多块。

在应用程序级别,我们很少捕获运行时异常,我认为这种做法是不好的。


1
在框架级别您将如何处理这些异常?
马修(Matthieu)

如果框架中有一个可以处理异常的UI层,则UI将显示某种错误消息,说明发生了某些错误。如果是一页javascript应用程序,则该应用程序可能会显示错误消息。诚然,只有在更深的一层确实无法从错误中恢复时,UI层才应该处理错误。
杰克·多伦多
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.