Java异常处理习惯用语...谁是对的以及如何处理它?

7
我目前与一位熟人存在技术分歧。简而言之,就是Java异常处理的这两种基本风格之间的差异:
选项1(我的):
try {
...
} catch (OneKindOfException) {
...
} catch (AnotherKind) {
...
} catch (AThirdKind) {
...
}

选项2(他的):
try {
...
} catch (AppException e) {
    switch(e.getCode()) {
    case Constants.ONE_KIND:
    ...
    break;
    case Constants.ANOTHER_KIND:
    ...
    break;
    case Constants.A_THIRD_KIND:
    ...
    break;
    default:
    ...
    }
}

他的论点 - 在我使用大量关于用户输入验证、异常处理、断言和合同等方面的链接来支持我的观点之后 - 归结为以下内容:
“这是一个好模型。自从我和我的一个朋友在1998年提出它以来,我一直在使用它,已经将近10年了。再仔细看一下,你会发现我们为了达成学术争论而做出的妥协非常有道理。”
有人能否给出一个毋庸置疑的理由,解释为什么选项1是正确的选择?

我猜他来自基于过程代码的世界,这不是他经常使用的唯一实践吧? - matt b
他的朋友是不是在98年从C语言转到Java语言的?他是否有一些放不下C语言的情结? - erickson
你应该添加标签“最佳实践”。 - Eddie
Eddie,你的声望足够了。如果Steven没有先你一步,你可以自己添加标签。 - Rob Kennedy
@Rob Kennedy:也许这并不必要,但我尽量避免编辑我回答的问题。 - Eddie
我的理解是,如果你无法让别人采用第一种方案,那就是因为你在“政治”上比他更弱势,并且在某些情况下你无能为力。我的意思是,第一种选项显然比第二种更好,不需要争论。也许那个人只是更有权力而已。 - OscarRyz
11个回答

19

当使用switch语句时,你的代码就不太面向对象。此外,还有更多出错的机会,例如忘记加"break;"语句,在添加新的Exception时忘记添加相应的case。

我觉得你的方法更易读,并且是所有开发人员都能立即理解的标准用法。

对我来说,使用你的方法需要太多样板代码,这些代码与实际处理异常无关,这是不能接受的。在实际程序逻辑周围有太多样板代码,使得代码难以阅读和维护。并且,使用不常见的用法会使代码更难理解。

但是,正如我之前所说,如果你修改被调用的方法以引发一个额外的Exception,你将自动知道必须修改你的代码,因为它将无法编译。然而,如果使用你的熟人的方法,并将被调用的方法修改为引发新的AppException,你的代码将不知道这种新变化,并且你的代码可能会通过进入不合适的错误处理过程而默默失败。这是假设你实际上记得放置一个默认值以至少进行处理而不被默默忽略。


switch语句是过时的遗物。 - Soviut

6
  • 以选项2的编码方式,任何意料之外的异常类型都会被吞噬!(这可以通过在默认情况下重新抛出异常来修复,但这可能是一个丑陋的方法——最好/更有效率的方式是一开始就不要捕获它)
  • 选项2是对选项1在底层最可能执行的操作的手动重现,即它忽略了使用旧构造体维护和可读性原因而应该避免的首选语法。换句话说,选项2是使用比语言结构提供的更丑陋的语法重新发明轮子。

显然,两种方法都可行;选项2仅仅被选项1支持的更现代的语法取代了。


4

我不知道是否有令人信服的论点,但初步想法如下:

  • 选项2可以正常工作,直到您尝试捕获未实现getCode()方法的异常。
  • 选项2鼓励开发人员捕获普通异常,这是一个问题,因为如果您没有为给定的AppException子类实现case语句,则编译器不会警告您。当然,您也可能遇到与选项1相同的问题,但至少选项1不积极鼓励这样做。

4
选项1:调用方可以选择准确地捕获并忽略所有其他异常。选项2:调用方需要记住重新抛出未明确捕获的所有异常。此外,选项1具有更好的自我记录功能,因为方法签名需要指定抛出的所有异常,而不是一个覆盖所有异常的单个异常类型。如果需要拥有全面的AppException,则其他异常类型可以从它继承。

3
在这两种情况下,我认为您可能过度使用异常。一般来说,我只在以下两种情况同时满足时才会抛出异常:
  1. 发生了无法在此处处理的意外情况。
  2. 有人会关心堆栈跟踪。
那么第三种方式呢?您可以使用一个enum来表示错误类型,并将其作为方法类型的一部分简单地返回。为此,您可以使用例如Option<A>Either<A, B>
例如,您会有:
enum Error { ONE_ERROR, ANOTHER_ERROR, THIRD_ERROR };

而不是

public Foo mightError(Bar b) throws OneException

您将拥有

public Either<Error, Foo> mightError(Bar b)

抛出/捕获有点像goto / comefrom。容易滥用。参见Go To语句被认为是有害的懒惰的错误处理


这很可爱——使用泛型的Haskell风格错误处理。我的主要反对意见是,总有人关心堆栈跟踪——即使它是顶级的“嘿,程序员,修复这个bug!”记录器。在我示例中省略的大多数部分可能会是“throw new RuntimeException(e)”。 - Steven Huwig
我同意,当出现真正的错误时,某人会关心堆栈跟踪。然后您肯定希望抛出未经检查的异常,因为任何合理的调用者都不应从中恢复。您希望尽快且尽可能大声地失败。 - Apocalisp

3

最有力的论据是,它破坏了封装性,因为现在我必须知道Exception子类的公共接口才能处理它引发的异常。JDK中这种“错误”的一个很好的例子是java.sql.SQLException,它暴露了getErrorCode和getSQLState方法。


1

我认为这取决于使用的程度。我肯定不会有一个“例外情况支配所有规则”,被所有东西抛出。另一方面,如果有一整个类似的情况几乎肯定会以相同的方式处理,但您可能需要区分它们以用于(例如)用户反馈目的,则选项2 仅适用于这些异常情况。它们应该非常狭窄 - 所以无论何时抛出一个“代码”都是合理的地方,所有其他人也应该被抛出。

对我来说关键的测试是:“是否有意义捕获带有一个代码的AppException,但希望让另一个代码保持未捕获?” 如果是的话,它们应该是不同的类型。


1

每个已检查的异常都是一个例外情况,必须处理以定义程序行为。不需要进入合同义务之类的事情,这只是一个有意义的简单问题。比如,你问店主某件物品的价格,结果发现该物品不出售。现在,如果你坚持只接受非负数值作为答案,那么永远不可能提供正确的答案。这就是使用已检查异常的关键点,你要求执行某些操作(例如产生响应),如果你的请求不能以有意义的方式执行,你就必须合理地计划处理。这就是编写强健代码的代价。

对于选项2,你完全掩盖了代码中“异常条件”的含义。除非它们永远不需要以不同的方式处理,否则你不应将不同的错误条件折叠成单个通用的AppException。你分支 getCode() 表明情况并非如此,因此对于不同的异常,请使用不同的异常。

我能看到 Option 2 的唯一真正优点是可以使用相同的代码块清晰地处理不同的异常。这篇好的博客文章讨论了 Java 中的这个问题。尽管如此,这是一个风格与正确性的问题,而正确性胜出。

0

如果选项2是这样的,我会支持它:

default:
  throw e;

语法可能有点丑陋,但是能够为多个异常执行相同的代码(即连续的情况)的能力要好得多。唯一让我感到困扰的是在抛出异常时生成一个唯一的 ID,并且系统肯定有待改进。


1
你会真正支持它而不是抱怨传统方式吗? - Steven Huwig

0
  1. 使用选项1时,不必知道代码和声明常量即可将异常抽象化。

  2. 我猜第二个选项只有在捕获一个特定的异常时才会改变为传统方式(即选项1),因此我认为存在不一致性。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接