为什么方法不应该引发多种类型的检查异常?


47

我们使用SonarQube分析我们的Java代码,它具有以下规则(设置为关键):

公共方法最多应引发一个检查异常

使用检查的异常会强制方法调用者通过传播错误或通过处理错误来处理错误。这使得这些异常完全成为方法API的一部分。

为了使调用者的复杂度保持合理,方法不应抛出一种以上的检查异常。”

声纳的另一点是这样的

公共方法最多应引发一个检查异常

使用检查的异常会强制方法调用者通过传播错误或通过处理错误来处理错误。这使得这些异常完全成为方法API的一部分。

为了使调用者的复杂度保持合理,方法不应引发超过一种检查异常。

如下代码:

public void delete() throws IOException, SQLException {      // Non-Compliant
  /* ... */
}

应该重构为:

public void delete() throws SomeApplicationLevelException {  // Compliant
    /* ... */
}

覆盖方法不受此规则检查,并允许引发多个检查的异常。

在阅读有关异常处理的文章时,我从没有遇到过这个规则/建议,也没有尝试找到有关该主题的一些标准,讨论等。我从CodeRach中发现的唯一东西是:一个方法最多应抛出多少个异常?

这是一个公认的标准吗?


7
行什么,你觉得呢?您从SonarQube引用的解释似乎很明智。您有任何怀疑的理由吗?
罗伯特·哈维

3
我写了很多代码,它们抛出一个以上的异常,并利用了许多库也抛出了一个以上的异常。另外,在有关异常处理的书籍/文章中,通常不会提出限制抛出异常数量的主题。但是,许多示例都显示了抛出/捕捉倍数,从而暗含了对该实践的认可。因此,我发现该规则令人惊讶,并希望对异常处理的最佳实践/哲学进行更多研究,而不仅仅是基本的操作方法示例。
sdoca

Answers:


32

让我们考虑您提供以下代码的情况:

public void delete() throws IOException, SQLException {      // Non-Compliant
  /* ... */
}

这里的危险是您编写的要调用的代码delete()如下所示:

try {
  foo.delete()
} catch (Exception e) {
  /* ... */
}

这也是不好的。并且它将被另一条标记捕获基本Exception类的规则捕获。

关键是不要编写使您想在其他地方编写错误代码的代码。

您遇到的规则是很常见的。 Checkstyle的设计规则中包含以下内容:

抛出计数

将throws语句限制为指定的计数(默认为1)。

原理:异常构成方法接口的一部分。声明一个方法以引发太多不同根源的异常会使异常处理繁重,并导致不良的编程实践,例如编写诸如catch(Exception ex)之类的代码。这种检查迫使开发人员将异常放入层次结构中,以便在最简单的情况下,调用者只需要检查一种类型的异常,但是可以在需要时专门捕获任何子类。

这准确地描述了问题,问题所在以及为什么不应该这样做。许多静态分析工具将识别并标记这是一个公认的标准。

虽然你可以根据语言的设计做到这一点,有可能是时候,这是应该做的正确的事情,这是东西,你应该看到,马上走“嗯,我为什么这样做呢?” 对于内部代码来说,每个人都受过足够严格的训练以至于永远不会接受,这是可以接受的catch (Exception e) {},但是我经常看到人们偷工减料,尤其是在内部情况下。

不要让使用您的班级的人想要编写错误的代码。


我应该指出,在Java SE 7和更高版本中,这的重要性降低了,因为单个catch语句可以捕获多个异常(从Oracle 捕获改进的类型检查并捕获多个异常类型和重新抛出异常)。

使用Java 6及更低版本,您将获得如下代码:

public void delete() throws IOException, SQLException {
  /* ... */
}

try {
  foo.delete()
} catch (IOException ex) {
     logger.log(ex);
     throw ex;
} catch (SQLException ex) {
     logger.log(ex);
     throw ex;
}

要么

try {
    foo.delete()
} catch (Exception ex) {
    logger.log(ex);
    throw ex;
}

Java 6的这些选项都不是理想的。第一种方法违反了DRY。多个块一次又一次地执行相同的操作-每个异常一次。您要记录异常并重新抛出它吗?好。每个异常的代码行相同。

由于多种原因,第二种选择更糟。首先,这意味着您正在捕获所有异常。空指针在那里被捕获(并且不应该)。此外,你重新抛出Exception这意味着该方法的签名是deleteSomething() throws Exception这只是让一个烂摊子进一步上涨的堆栈使用你的代码的人现在被迫catch(Exception e)

在Java 7,这是不是因为,因为你可以做,而不是重要的:

catch (IOException|SQLException ex) {
    logger.log(ex);
    throw ex;
}

此外,类型检查是否确实捕获了抛出的异常的类型:

public void rethrowException(String exceptionName)
throws IOException, SQLException {
    try {
        foo.delete();
    } catch (Exception e) {
        throw e;
    }
}

类型检查器将认识到,e可能是类型IOExceptionSQLException。我仍然不太热衷于使用这种样式,但是它并没有像Java 6那样导致糟糕的代码(在这种情况下,它将迫使您将方法签名作为异常扩展的超类)。

尽管进行了所有这些更改,但是许多静态分析工具(Sonar,PMD,Checkstyle)仍在执行Java 6样式指南。这不是一件坏事。我倾向于同意仍然强制执行这些警告,但是您可以根据您的团队对它们的优先级排序,将其优先级更改为主要或次要。

如果异常,应选中或取消选中...这是一个事[R Ë 一个牛逼 的辩论,人们可以很容易地发现无数的博客文章占用的说法两侧。但是,如果使用检查的异常,则至少在Java 6下,应该避免抛出多个类型。


接受它作为答案,因为它回答了我关于它是公认的标准的问题。但是,我仍在阅读各种pro / con讨论,从他的答案中提供的Panzercrisis链接开始,以确定我的标准是什么。
sdoca

“类型检查器将识别出e只能是IOException或SQLException类型。”:这是什么意思?当引发另一种类型的异常时会发生什么foo.delete()?它仍然被捕获并重新抛出吗?
乔治

@Giorgio如果delete在该示例中抛出IOException或SQLException以外的已检查异常,将是编译时错误。我要说明的关键点是,调用rethrowException的方法仍将获得Java 7中Exception的类型。在Java 6中,所有这些都合并为通用Exception类型,这使静态分析和其他编码人员感到不愉快。

我懂了。对我来说似乎有点令人费解。我会发现更直观的做法是禁止catch (Exception e)并将其强制为“ catch (IOException e)或” catch (SQLException e)
乔治

@Giorgio从Java 6向前迈出的一步是尝试使编写良好的代码更加容易。不幸的是,编写错误代码的选择将持续很长时间。请记住,Java 7可以catch(IOException|SQLException ex)代替。但是,如果您只是要抛出异常,则允许类型检查器传播异常的实际类型,同时简化代码并不是一件坏事。

22

理想情况下,您只想抛出一种类型的异常的原因是,否则可能会违反“ 单一职责依赖倒置”原则。让我们用一个例子来演示。

假设我们有一个从持久性中获取数据的方法,并且该持久性是一组文件。由于我们正在处理文件,因此可以有一个FileNotFoundException

public String getData(int id) throws FileNotFoundException

现在,我们的需求发生了变化,我们的数据来自数据库。现在,而不是FileNotFoundException(因为我们不处理文件),所以抛出了SQLException

public String getData(int id) throws SQLException

现在,我们将遍历所有使用我们方法的代码,并更改要检查的异常,否则该代码将无法编译。如果我们的方法被广泛调用,那么可能会有很多改变/需要其他改变。这需要很多时间,人们不会感到高兴。

依赖倒置说我们真的不应该抛出这些异常中的任何一个,因为它们暴露了我们正在封装的内部实现细节。调用代码需要知道我们正在使用什么类型的持久性,何时才真正应该担心是否可以检索记录。相反,我们应该抛出一个异常,该异常以与通过API公开时相同的抽象级别传达错误:

public String getData(int id) throws InvalidRecordException

现在,如果我们更改内部实现,则可以将异常包装到中InvalidRecordException并将其传递(或不包装它,而只抛出一个new InvalidRecordException)。外部代码不知道或不在乎正在使用哪种类型的持久性。全部封装。


至于单一职责,我们需要考虑引发多个不相关异常的代码。假设我们有以下方法:

public Record parseFile(String filename) throws IOException, ParseException

关于这种方法我们能说些什么?我们可以从签名中得知它打开了一个文件并进行了解析。当我们看到一个连词时,如方法说明中的“和”或“或”,我们知道它所做的不只是一件事。它有不止一个责任。具有多个职责的方法很难管理,因为如果任何职责更改,它们都可以更改。相反,我们应该分解方法,使它们负有单一责任:

public String readFile(String filename) throws IOException
public Record parse(String data) throws ParseException

我们已经从解析数据的责任中提取了读取文件的责任。这样做的一个副作用是,我们现在可以将任何String数据从任何源(内存,文件,网络等)传递到解析数据。我们现在也可以parse更轻松地进行测试,因为我们不需要磁盘上的文件进行测试。


有时,我们确实可以从一个方法中抛出两个(或多个)异常,但是如果我们坚持使用SRP和DIP,遇到这种情况的时间就会越来越少。


我完全同意按照您的示例包装较低级别的异常。我们定期这样做,并抛出MyAppExceptions的差异。我引发多个异常的示例之一是尝试更新数据库中的记录时。此方法引发RecordNotFoundException。但是,仅当记录处于特定状态时才可以对其进行更新,因此该方法还会引发InvalidRecordStateException。我认为这是有效的,并为呼叫者提供了有价值的信息。
sdoca 2014年

@sdoca如果您的update方法是可以实现的,并且异常处于适当的抽象级别,那么是的,听起来您确实需要抛出两种不同类型的异常,因为存在两种例外情况。那应该是可以抛出多少个异常的度量,而不是这些(有时是任意的)linter规则。
cbojar 2014年

2
但是,如果我有一个从流中读取数据并对其进行解析的方法,那么我不能在不将整个流读入缓冲区的情况下分解这两个函数,这可能很麻烦。此外,可以将决定如何正确处理异常的代码与进行读取和解析的代码分开。当我编写读取和解析代码时,我不知道调用我的代码的代码可能想如何处理两种类型的Exception,因此我需要让它们同时通过。
user3294068

+1:我非常喜欢这个答案,特别是因为它从建模的角度解决了这个问题。通常没有必要使用其他惯用法(例如catch (IOException | SQLException ex)),因为真正的问题出在程序模型/设计中。
Giorgio

3

我记得前一段时间在玩Java时有点儿摆弄,但是直到我读完您的问题之前,我才真正意识到选中和未选中之间的区别。我很快在Google上找到了这篇文章,并且引起了一些明显的争议:

http://tutorials.jenkov.com/java-exception-handling/checked-or-unchecked-exceptions.html

话虽这么说,这个家伙提到的带有检查异常的问题之一是(而且我从Java开始就亲自遇到过这个问题),如果您继续向throws方法声明中的子句添加一堆检查异常,不仅当您移至更高级别的方法时,您是否必须投入更多的样板代码来支持它,但是当您尝试向更低级别的方法中引入更多的异常类型时,它也会使问题变得更大,并且破坏兼容性。如果将检查的异常类型添加到较低层的方法,则必须重新运行代码并调整其他几个方法声明。

文章中提到的缓解方法之一(作者个人并不喜欢这种方法)是创建基类异常,将throws子句限制为仅使用它,然后在内部引发它的子类。这样,您可以创建新的已检查异常类型,而不必重新执行所有代码。

本文的作者可能并不太喜欢这种方法,但是根据我的个人经验(特别是如果您可以查找所有子类的含义),这是完全合理的,我敢打赌,这就是为什么您会得到建议的原因将所有内容都限制为一种检查异常类型。更重要的是,您提到的建议实际上允许在非公共方法中使用多种检查异常类型,如果这是它们的动机(甚至是其他目的),则这是非常有意义的。如果这只是私有方法或类似的方法,那么当您更改一件小事情时,您将不会运行一半的代码库。

您确实在很大程度上询问这是否是一个可接受的标准,但是在您提到的研究,这篇经过深思熟虑的文章之间,以及仅从个人编程经验上讲,它似乎并没有以任何方式脱颖而出。


2
为什么不仅仅声明throws Throwable并完成它,而不是发明自己的所有层次结构?
Deduplicator

@Deduplicator这也是作者也不喜欢这个主意的原因。他只是想,如果您要这么做,那么最好还是使用所有未选中的选项。但是,如果使用API​​的任何人(可能是您的同事)都有从您的基类派生的所有子类异常的列表,那么我至少可以让他们知道所有预期的异常是有一点好处的。在某些子类集中;那么如果他们觉得其中一个比其他人更“易于处理”,那么他们就不会轻易放弃专门针对它的处理程序。
Panzercrisis

通常,检查异常是因果报应的原因很简单:它们具有病毒性,会感染每个用户。指定有意的最可能的规范只是说所有异常均未选中的一种方式,从而避免了混乱。是的,记录您可能要处理的是一个好主意,为文件:只要知道哪些异常,可能来自一个功能被严格限制值的(除了根本就没有/也许有,但Java不允许反正) 。
Deduplicator

1
@Deduplicator我不同意这个想法,我也不一定赞成。我只是在谈论OP给出的建议的方向。
Panzercrisis

1
感谢您的链接。这是阅读该主题的绝佳起点。
sdoca

-1

当有多个合理的事情要做时,抛出多个检查异常是有意义的。

例如,假设您有方法

public void doSomething(Credentials cred, Work work) 
    throws CredentialsRequiredException, TryAgainLaterException{...}

这违反了pne例外规则,但是很有意义。

不幸的是,通常发生的是类似

void doSomething() 
    throws IOException, JAXBException,SQLException,MyException {...}

在这里,调用者几乎没有机会根据异常类型执行特定的操作。因此,如果我们想让他意识到这些方法有时甚至会出错,则抛出SomethingMightGoWrongException是更好的选择。

因此,规则最多只能检查一个例外。

但是,如果您的项目使用的设计中存在多个有意义的检查异常,则此规则不适用。

旁注:实际上几乎所有地方都可能出问题,因此可以考虑使用?扩展了RuntimeException,但是“我们都犯错”和“这在谈论外部系统,有时会停机,处理它”之间是有区别的。


1
“要做的许多合理的事情”几乎不符合srp的要求 -这一点已在先前的答案中提出
2014年

是的 您可以检测一个函数中的一个问题(处理一个方面),而检测另一问题中的另一个问题(也处理一个方面),这个问题是从处理一个问题的一个函数调用的,即调用这两个函数。并且呼叫者可以在一层尝试中捕获(在一个功能中)处理一个问题并向上传递另一个问题,也可以单独处理该问题。
user470365
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.