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

评论

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

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

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

功能接口也很明显:内置功能接口(即Function,Predicate等)没有参数化的throws子句。这意味着您需要在任何stream()方法的内部循环中捕获,包装和重新抛出所有检查的异常。这本身就为我提供了关于检查异常是否不是一个好主意的平衡。

@Songo并不是特别正确,您可以将基础实现的异常与您自己层的异常一起包装,并传播这些异常。

#1 楼

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

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

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

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

评论


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

–迈克尔·伯格沃德(Michael Borgwardt)
11年11月24日在13:31

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

– DavidS
2015年9月11日在21:20

任何写空的捕捞块的人都应当场开除,并只限于在地牢中的清洁工作

– ar鼠
20-05-16在18:51

#2 楼

可以很好。请阅读以下onjava.com文章:



大多数情况下,客户端代码不能对SQLException做任何事情。
不要犹豫,将它们转换为未经检查的异常。考虑下面的代码段:


public void dataAccessCode(){
  try{
      ..some code that throws SQLException
  }catch(SQLException ex){
      ex.printStacktrace();
  }
} 



该catch块只是抑制了异常,什么也不做。理由是我的客户对SQLException不能做任何事情。如何以以下方式处理它?<​​br />

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


最近,包括布鲁斯在内的几位广受好评的专家埃克尔(Eckel)和罗德(Rod)
约翰逊(Johnson)公开表示,尽管他们最初完全同意受检查的异常的正统立场,但他们
得出结论,仅使用受检查的异常并不如<最初出现的想法,而检查异常已成为许多大型项目的重要问题来源。埃克尔(Eckel)采取了更为极端的观点,认为所有异常均应不受检查。约翰逊的观点较为保守,但仍然暗示

(值得注意的是,几乎肯定拥有
使用Java技术的丰富经验的C#架构师选择从语言设计,使所有异常都成为未检查的异常。但是,它们确实为以后实现
检查的异常留有空间。)


也来自相同的链接:


使用未经检查的异常的决定是一个复杂的决定,并且
显然没有明显的答案。 Sun的建议是什么都不要使用它们,C#方法(Eckel和其他人都同意)
是将它们用于一切。其他人说,“有中间立场。”


#3 楼

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

评论


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

– RoflcoptrException
2011年11月23日在16:58

@加里·布恩(Gary Buyn):许多人认为检查异常是一种失败的语言设计实验,应谨慎使用,而不是出于习惯。

–迈克尔·伯格沃德(Michael Borgwardt)
11年11月23日在20:17

@Gary Buyn:这是一篇很好地概述了辩论的文章:ibm.com/developerworks/java/library/j-jtp05254/index.html请注意,在Java引入此功能15年后,没有其他语言采用它, C ++不赞成使用类似功能。

–迈克尔·伯格沃德(Michael Borgwardt)
2011-11-23 20:56

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

–迈克尔·伯格沃德(Michael Borgwardt)
2011年11月24日13:26

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

– K.L.
2012年11月15日15:32

#4 楼

这要视情况而定。

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

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

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

评论


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

–c_maker
11年11月23日在22:31

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

–超级猫
2014年7月9日在20:10

感谢您提及异常处理层-例如在webapp中,是过滤器。

–杰克·多伦多
2014年10月3日在17:11

#5 楼

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

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

假设稍后不会捕获并忽略RuntimeException,那么它就离OnErrorResumeNext不远了。

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

评论


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

– Michael K
2011-11-23 19:47

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

– Tim Seguine
17年5月10日在15:20

#6 楼

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...
}


为了固定初始容量或负载因子后,您可以编辑代码以确保传递正确的值。不依赖于某个远程服务器正在运行,磁盘的当前状态,文件,或其他程序。用无效参数调用构造函数取决于调用代码的正确性,无论是导致无效参数的错误计算,还是导致错误遗漏的错误流程。

什么时候引发检查异常?当问题可以恢复而无需更改代码时,将引发已检查的异常。或者换句话说,当错误与状态相关且代码正确时,您将引发一个已检查的异常。

现在,“ recover”一词在这里可能很棘手。这可能意味着您找到了实现该目标的另一种方法:例如,如果服务器没有响应,则应尝试下一个服务器。如果您的情况下可以进行这种恢复,那很好,但这并不是恢复的唯一手段-恢复可能只是向用户显示一个错误对话框,说明发生了什么,或者如果是服务器应用程序,则可能是发送电子邮件给管理员,甚至只是适当而简洁地记录错误。

让我们以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
}


经检查的异常是一种静态分析工具,它们使程序员可以清楚地了解某个调用中可能出了什么问题,而无需他们学习实现或经历试错过程。这样可以轻松确保不会忽略任何错误流。将已检查的异常重新抛出为运行时异常会不利于此节省劳动力的静态分析功能。

还值得一提的是,调用层具有更宏大的事物方案的更好的上下文,如上面所演示的。呼叫dataAccessCode的原因可能有很多,呼叫的特定原因仅对呼叫者可见-因此,它能够在失败后正确恢复时做出更好的决定。

现在我们已经清楚了这一区别,我们可以继续推断何时可以将检查后的异常作为运行时异常重新抛出。一个检查的异常作为RuntimeException?当您使用的代码假定依赖于外部状态时,但可以清楚地断言它不依赖于外部状态。

请考虑以下内容:

public void loadDataAndShowUi() {
    try {
        Data data = dataAccessCode();
        showUiForData(data);
    } catch(SQLException e) {
        // Recover by showing an error alert dialog
        showCantLoadDataErrorDialog();
    }
}


在此示例中,代码正在传播IOException,因为Reader的API设计为访问外部状态,但是我们知道StringReader实现不访问外部状态。在此范围内,我们可以肯定地断言调用中涉及的部分不会访问IO或任何其他外部状态,我们可以安全地将异常作为运行时异常抛出,而不会引起不了解我们的实现的同事(并且可能假设IO访问代码将抛出IOException)。


严格检查依赖于外部状态的异常的原因是,它们是不确定的(与逻辑相关的异常不同,逻辑上的异常将每次代码版本每次都可复制)。例如,如果尝试除以0,则始终会产生异常。如果不除以0,则永远不会产生异常,也不必处理该异常情况,因为它永远不会发生。但是,在访问文件的情况下,一次成功并不意味着您下次会成功-用户可能已更改权限,另一个进程可能已删除或修改了该文件。因此,您始终必须处理这种特殊情况,否则您可能会遇到错误。

#7 楼

对于独立应用程序。当您知道您的应用程序无法处理该异常时,您可以代替抛出已检查的RuntimeException,引发错误,让应用程序崩溃,希望报告错误并修复您的应用程序。 (有关已检查和未检查的赞成与反对的更深入讨论,请参见走私的答案。)

#8 楼

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

#9 楼

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

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


通过参数传递从调用方到被调用方。
从被调用方到其调用方,作为返回值。

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

在某些情况下,有必要将信息发送到“远程”功能,而中间没有任何人“知道”。正是在必须使用异常的情况下。异常是从提升者(您的代码的任何部分可能包含throw语句)发送给处理程序的秘密消息(您的代码的任何部分可能包含与catch n兼容的throw块)的秘密消息。 />
受检查的异常会破坏该机制的机密性,并破坏其存在的原因。如果函数可以让调用者“知道”一条信息,则直接将该信息作为返回值的一部分发送。

评论


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

–超级猫
15年7月27日在18:06

#10 楼

在一个布尔型问题中,很难通过两个有争议的答案来回答不同的问题,但是我想向您提供一个观点,即即使在很少的地方提到它,对于它的重要性也没有足够的强调。 >多年来,我发现总是有人对一个琐碎的问题感到困惑,他们对某些基本原理缺乏了解。

分层。一个软件应用程序(至少应该是)一堆又一堆的层。对良好分层的一个重要期望是,较低的层可以为高层的潜在多个组件提供功能。业务模型。

如果您的业务层想执行rest调用,请稍等。我为什么这么说?为什么不说HTTP请求或TCP事务或发送网络程序包?因为这些与我的业务层无关。我不会处理它们,也不会查看它们的细节。如果它们深深地扎根于我作为例外的原因,并且我不想知道它们甚至存在,那我就完全可以了。如果明天我想更改处理特定于TCP协议的详细信息的下划线传输协议,则意味着我的REST抽象不能很好地从特定实现中抽象出自身。

从一个层到另一个层过渡异常时,重要的是重新审视异常的各个方面,以及它将对当前层提供的抽象有何意义。可能是将异常替换为其他异常,也可能合并了多个异常。

当然,您提到的实际位置是否有意义,但总的来说-是的,这样做可能是一件好事做。

#11 楼

这可能视情况而定。在某些情况下,明智的做法是执行朋友正在做的事情,例如,当您为某些客户端公开api时,并且您希望客户端至少了解实现细节时,您知道某些实现例外可能特定于实现的详细信息,并且无法公开给客户端。特殊条件。例如,Integer.parseInt(String)接收一个字符串并返回它的等价整数,如果该字符串不是数字,则抛出NumberFormatException。现在,假设通过此方法转换了具有字段age的表单提交,但是客户端已经确保了其验证,因此没有必要强制检查异常。

#12 楼

这里确实有几个问题


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

一般的经验法则是,调用者可以预期的异常捕捉并从中恢复。其他例外情况(唯一合理的结果是中止整个操作的情况,或者您认为它们不太可能导致担心不专门处理它们的情况除外)。值得捕获和恢复的异常不同于您使用的API。有时上下文很重要,在一种情况下值得处理的异常可能在另一种情况下不值得处理。有时,您的手被现有接口所迫。因此,是的,出于正当的理由,有必要将已检查的异常转换为未检查的异常(或另一种类型的已检查的异常)


如果您要将未检查的异常转换为已检查的异常

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

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

#13 楼

在我看来,

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

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

评论


在框架级别您将如何处理这些异常?

–马修(Matthieu)
2011年12月13日,1:15

如果框架中存在可以处理异常的UI层,则UI将显示某种错误消息,说明发生了某些错误。如果是一页javascript应用程序,则该应用程序可能会显示错误消息。诚然,只有在更深的一层确实无法从错误中恢复时,UI层才应该处理错误。

–杰克·多伦多
2014年10月3日在17:14