【问题标题】:Extending Throwable in Java在 Java 中扩展 Throwable
【发布时间】:2010-05-03 03:29:24
【问题描述】:

Java 允许您创建一个全新的 Throwable 子类型,例如:

public class FlyingPig extends Throwable { ... }

现在,很少,我可能会这样做:

throw new FlyingPig("Oink!");

当然还有其他地方:

try { ... } catch (FlyingPig porky) { ... }

我的问题是:

  • 这是个坏主意吗?如果是这样,为什么?
    • 如果这是一个坏主意,可以采取什么措施来防止这种子类型化?
    • 既然无法预防(据我所知),可能会导致什么灾难?
  • 如果这不是一个坏主意,为什么不呢?
    • 您如何利用extends Throwable这一事实做出有用的事情?

提议的场景 #1

真的想要做这样的事情的场景具有以下属性:

  • “事件”是最终发生的事情。这是预期的。它绝对不是Error,也没有关于它何时发生的Exception-al。
    • 因为是意料之中的,所以会有catch等着呢。它不会“溜走”任何东西。它不会“逃避”对catch 通用Exception 和/或Error 的任何尝试。
  • “事件”发生极少
  • 发生这种情况时,通常会有很深的堆栈跟踪。

所以也许现在我想说的很清楚了:FlyingPig 是穷举递归搜索的结果

要搜索的对象存在:只要在搜索空间的大海中找到它。搜索过程将是一个漫长的过程,因此异常处理相对昂贵的成本可以忽略不计。事实上,使用boolean isFound 标志的传统控制流构造替代方案可能更昂贵,因为它必须在整个搜索过程中不断检查,很可能在递归的每个级别。此检查将在 99.99% 的情况下失败,但传播终止条件是绝对必要的。在某种程度上,虽然有效,但检查效率低下

当找到寻找的对象时,只需throw-ing 一个FlyingPig,您就不必使用boolean isFound 标志的管理来弄乱代码。不仅在这方面代码更干净,而且由于这个遗漏,它可能运行得更快。

总而言之,选择是在这两者之间:

  • 传统的控制流方法
    • 使用boolean isFound,不断检查
    • 99.99% 的情况下,支票是“浪费”,因为它仍然是 false
    • 当它最终变为 true 时,您将停止递归,并且您必须确保可以正确地展开到初始调用。
  • FlyingPig 方法
    • 不要打扰任何boolean isFound
    • 如果找到,只需throw new FlyingPig();这是预期的,所以会有一个catch
    • 不管理boolean标志,不浪费检查是否需要继续,不记账手动展开递归等。

问题:

  • 这种(ab)使用异常的技术是否有效? (有名字吗?)
  • 如果有效,应该FlyingPig extends Throwable 还是Exception 就可以了? (即使它的情况并没有什么特别之处?)

【问题讨论】:

  • 有趣的想法,我只是在想这是否可以用来让JVM同步线程。捕获 throwable 是异步的吗?
  • 我曾经使用过class Finished extends Exception,原因完全相同:突破长递归搜索的性能。但我从来没有通过实际测量性能来证明这个原因。
  • @Christian:终于!和我有同样想法的人!我可能应该更进一步并进行基准测试。可能有一天会。

标签: java exception-handling throwable


【解决方案1】:

我会说这是一个非常糟糕的主意。很多代码是在假设你捕获ErrorException 时实现的,你已经捕获了所有可能的异常。大多数教程和教科书都会告诉你同样的事情。通过创建Throwable 的直接子类,您可能会产生各种维护和互操作性问题。

我想不出扩展Throwable 的充分理由。改为扩展ExceptionRuntimeException

编辑 - 响应 OP 提出的场景 #1。

异常是处理“正常”流控制的一种非常昂贵的方法。在某些情况下,我们说的是 数千 条额外的指令,用于创建、抛出和捕获异常。如果您要忽略公认的智慧并使用异常进行非异常流控制,请使用Exception 子类型。试图通过将 is 声明为 Throwable 的子类型来假装某事是“事件”而不是“异常”是不会取得任何成果的。

但是,将异常与错误、错误、错误等混为一谈是错误的。使用Exception 的子类来表示“不是错误、错误、错误或其他的异常事件”没有错。关键是事件要异常;即不寻常,很少发生,...

总而言之,FlyingPig 可能不是错误,但这不是不将其声明为 Exception 的子类型的理由。

【讨论】:

  • +1,但我想说这几乎总是一个坏主意,除非你知道这正是你想要做的。
  • @Stephen Denne - 在 10 多年的 Java 编码中,我还没有遇到过扩展 Throwable 是个好主意的情况。诚然,这种情况可能存在并非不可能......
  • 关于成本:我可能应该对此进行基准测试,但我认为从长远来看,数以千万计的boolean 测试可能比一次异常处理更昂贵。就像我已经强调的那样,搜索是一个漫长的过程:可能需要几分钟,甚至几个小时。另请参阅我对 Pascal 答案的评论。
  • @polygenelubricants - 这个用例对于异常来说听起来很合理,尤其是如果它显着简化了代码。
  • 什么时候扩展 RuntimeException 类?
【解决方案2】:

这种(ab)使用异常的技术有效吗? (有名字吗?)

在我看来,这不是一个好主意:

  • 它违反了principle of least astonishment,它使代码更难阅读,尤其是在没有任何异常的情况下。
  • 在 Java 中抛出异常是一项非常昂贵的操作(但在这里可能不是问题)。

异常应该只是not be used for flow control。如果我必须为这种技术命名,我会称其为代码异味或反模式。

另见:

如果有效,FlyingPig 应该扩展Throwable,还是Exception 就可以了? (即使它的情况没有什么异常?)

在某些情况下,您可能希望捕获 Throwable 不仅要捕获Exception 还要 Error,但这种情况很少见,人们通常不会抓住Throwable。但我没能找到您想要抛出Throwable 的子类的情况。

而且我还认为扩展Throwable 不会让事情看起来不那么“exception-al”,它们会让事情看起来更糟——这完全违背了意图。

所以总结一下,如果你真的想扔东西,扔一个Exception的子类。

另见:

【讨论】:

  • +1,但您没有解决这样一个事实,即处理很少发生的异常可能比在 99.99% 的时间无效的传统控制流上浪费时间更便宜。另外,当你 throwFlyingPig 时,你 catchFlyingPig。我从来没有提议过catch (Throwable t)(我同意这是个坏主意)。
  • @polygenelubricants 澄清一下,我同意你的第一句话,这里的性能成本不是一个好的论据。但是,为非异常的东西抛出异常(带一点 e)还是令人惊讶的。在我看来,这不是大多数代码读者所期望的。关于第二点,我的意图不是说你向catch (Throwable t) 求婚——你没有——但这似乎是我对Throwable 的唯一合理使用(抓住它,而不是扔它)。我已经更新了我的答案,希望事情更清楚。
  • 您能解释一下吗?“抛出异常在 Java 中是一项非常昂贵的操作。”?或者至少提供参考。据我所知,填充堆栈跟踪的成本很高,但异常处理的所有其他部分都没有那么糟糕。
  • @Jack 我的意思是整个throw new Exception() 都很昂贵。关于参考,例如检查Java Performance Tuning(但是任何关于“Java+performance+cost+exception”的搜索都会找到很多)。
  • private static final FlyingPig SHARED_EXCEPTION = (FlyingPig) new Flying().fillInStackTrace(); 后跟 throw SHARED_EXCEPTION; 会避免填充堆栈跟踪,不是吗? (好的,如果您希望异常具有状态,则需要一个“异常池”并管理多线程,但是如果您要扔飞猪,那么您可能对任何事情都足够疯狂了...)@Pascal 谢谢供参考。
【解决方案3】:

这是来自 HotSpot 架构师 John Rose 的博客文章:

http://blogs.oracle.com/jrose/entry/longjumps_considered_inexpensive

这是关于流控制的“滥用”异常。用例略有不同,但是.. 简而言之,它工作得非常好 - 如果您预先分配/克隆您的异常以防止创建堆栈跟踪。

如果对客户“隐藏”,我认为这种技术是合理的。 IE,你的 FlyingPig 永远不能离开你的库(所有公共方法都应该传递地保证不会抛出它)。保证这一点的一种方法是将其设置为已检查的异常。

我认为扩展 Throwable 的唯一理由是因为您希望允许人们传入具有 catch(Exception e) 子句的回调,并且希望您的结果被他们忽略。我可以买那个...

【讨论】:

  • “我可以买那个……”。我不这样做是因为 1)他们不应该捕获异常,2)他们可以(或者)捕获 Throwable,以及 3)在 javadocs 中说“不要在回调中捕获异常”更简单。
【解决方案4】:

org.junit.Test 注释包括 None 类,它扩展了 Throwable 并用作 expected 注释参数的默认值。

【讨论】:

  • 太聪明了一半,IMO。 None 同样可以被声明为Exception 的常规子类。当然,这不是在其他情况下可以效仿的例子。
【解决方案5】:

如果您能证明 FlyingPig 与 Error 和 Exception 的不同之处在于它不适合作为任何一个的子类,那么创建它从根本上没有错。

我能想到的最大问题是在务实的世界里,有时有正当的理由去捕捉 java.lang.Exception。您的新型 throwable 将飞过 try-catch 块,这些块具有抑制(或记录、包装等)所有可能的非致命问题的合理期望。

另一方面,如果您正在对一个un合理地抑制 java.lang.Exception 的旧系统进行维护,您可以绕开它作弊。 (假设真诚地呼吁时间来实际修复它被拒绝)。

【讨论】:

    【解决方案6】:

    随着这个问题的发展,我明白了 我误解了 原来的问题,所以一些 其他答案可能更多 相关的。我会留下这个答案 在这里,因为它可能仍然有助于 其他有类似问题的人, 并在搜索时找到此页面 回答他们的问题。

    扩展 Throwable 以抛出(和处理)自定义异常并没有错。但是,您应该牢记以下几点:

    • 尽可能使用最具体的异常是个好主意。它将允许调用者以不同的方式处理不同类型的异常。记住异常的类层次结构很重要,因此您的自定义异常应扩展另一种类型的 Throwable,该类型尽可能接近您的异常。
    • 扩展 Throwable 可能级别太高。尝试扩展 Exception 或 RuntimeException (或更接近您引发异常的原因的较低级别的异常)。请记住 RuntimeException 和 Exception 之间的区别。
    • 对引发异常(或异常的子类)的方法的调用需要包装在能够处理异常的 try/catch 块中。这适用于您预计由于可能无法控制的情况(例如,网络中断)而出现问题的情况。
    • 对引发 RuntimeException(或其子类)的方法的调用不需要包装在可以处理异常的 try/catch 块中。 (当然可以,但没必要。)这更适用于确实不应该出现的异常。

    因此,假设您的代码库中有以下异常类:

    public class Pig extends Throwable { ... }
    public class FlyingPig extends Pig { ... }
    public class Swine extends Pig { ... }
    public class CustomRuntimeException extends RuntimeException { ... }
    

    还有一些方法

    public void foo() throws Pig { ... }
    public void bar() throws FlyingPig, Swine { ... }
    // suppose this next method could throw a CustomRuntimeException, but it
    // doesn't need to be declared, since CustomRuntimeException is a subclass
    // of RuntimeException
    public void baz() { ... } 
    

    现在,您可以编写一些代码来调用这些方法,如下所示:

    try {
        foo();
    } catch (Pig e) {
        ...
    }
    
    try {
        bar();
    } catch (Pig e) {
        ...
    }
    
    baz();
    

    请注意,当我们调用bar() 时,我们可以捕获Pig,因为FlyingPigSwine 都扩展了Pig。如果您想做同样的事情来处理任何一个异常,这很有用。但是,您可以以不同的方式处理它们:

    try {
        bar();
    } catch (FlyingPig e) {
        ...
    } catch (Swine e) {
        ...
    }
    

    【讨论】:

    • 这并没有解决问题,即直接继承很少捕获的Throwable类是否安全。
    【解决方案7】:

    Play! framework 使用类似的东西来处理请求。请求处理经过许多层(路由、中间件、控制器、模板渲染),在最后一层渲染的 HTML 是wrapped in a throwable 并被抛出,最顶层捕获、解包并发送给客户端。因此,所涉及的许多层中的任何方法都不需要显式返回响应对象,也不需要将响应对象作为参数传递以进行传播和修改。

    我对细节有点粗略。您可以查看 Play 的代码!详细框架。

    【讨论】:

    • 你知道这个成语的名字吗?
    • 我不知道这个成语的名字。我是在阅读框架源码时偶然发现的。
    • 感觉就像一个虫洞 - 在堆栈的一个点消失并出现在不同的点,而无需穿过中间的部分。不确定这是否是一件好事。
    • 您答案中的第二个链接被破坏了。 (我不确定它是否是永久性的……)
    猜你喜欢
    • 2014-10-13
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-09-24
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多