【问题标题】:Is it safe to store an instance of an Exception and reuse it?存储异常实例并重用它是否安全?
【发布时间】:2013-02-26 13:43:14
【问题描述】:

是否安全:

public class Widget {

    private static final IllegalStateException LE_EXCEPTION
            = new IllegalStateException("le sophisticated way");

    ...

   public void fun() {
      // some logic here, that may throw
      throw LE_EXCEPTION;
   }

   ....
}
  1. 保留一个异常实例
  2. 在需要时使用它(抛出)

而不是每次都抛出 new 异常?

我对它是否安全感兴趣

安全的意思是:没有内存损坏,没有 JVM 抛出的额外异常,没有丢失的类,没有加载错误的类 (...)。注意:异常将通过网络(远程)抛出。

其他问题(可读性、保存实例的成本)并不重要。

【问题讨论】:

  • 我认为这是多余的。除非你的代码经常抛出它
  • 它总是有一个异常实例,即使你的代码没有抛出异常

标签: java exception exception-handling


【解决方案1】:

这是不安全的,除非异常是不可变的(即 enableSuppression=writableStackTrace=false)。

如果异常不是不可变的,它可以被捕获器修改 - 设置新的堆栈跟踪或添加抑制的异常。如果有多个 catcher 试图修改异常,就会出现混乱。

令人惊讶的是,Throwable 实际上是线程安全的,因为天知道是什么。所以如果一个异常被多个线程修改,至少不会出现灾难性的故障。但是会出现逻辑故障。

如果应用不断向这个长期存在的异常添加抑制的异常,内存泄漏也是可能的。

【讨论】:

    【解决方案2】:

    这取决于您对“安全”的定义。该异常会产生误导性的堆栈跟踪,我不会称之为“安全”。考虑:

    public class ThrowTest {
        private static Exception e = new Exception("t1"); // Line 2
    
        public static final void main(String[] args) {
            ThrowTest tt;
    
            tt = new ThrowTest();
            try {
                tt.t1();
            }
            catch (Exception ex) {
                System.out.println("t1:");
                ex.printStackTrace(System.out);
            }
            try {
                tt.t2();                                  // Line 16
            }
            catch (Exception ex) {
                System.out.println("t2:");
                ex.printStackTrace(System.out);
            }
        }
    
        private void t1() 
        throws Exception {
            throw this.e;
        }
    
        private void t2() 
        throws Exception {
            throw new Exception("t2");                    // Line 31
        }
    }
    

    有这个输出:

    $ java ThrowTest
    t1:
    java.lang.Exception: t1
        在 ThrowTest.(ThrowTest.java:2)
    t2:
    java.lang.Exception: t2
        在 ThrowTest.t2(ThrowTest.java:31)
        在 ThrowTest.main(ThrowTest.java:16)
    

    请注意t1 方法是如何从第一个测试用例的堆栈跟踪中完全丢失的。根本没有有用的上下文信息。

    现在,您可以在投掷前使用fillInStackTrace 填写该信息:

    this.e.fillInStackTrace();
    throw this.e;
    

    ...但这只是为自己工作(有时您忘记工作)。根本没有任何好处。并不是所有的异常都允许你这样做(一些异常使堆栈跟踪只读)。


    您在 cmets 的其他地方已经说过,这是为了避免“代码重复”。拥有一个异常生成器功能会好多了

    private IllegalStateException buildISE() {
        return new IllegalStateException("le sophisticated way");
    }
    

    (如果你愿意,可以是static final。)

    然后这样扔:

    throw buildISE();
    

    这样可以避免代码重复,而不会误导堆栈跟踪和不必要的异常实例。

    这就是应用到上面的样子:

    public class ThrowTest {
    
        public static final void main(String[] args) {
            ThrowTest tt;
    
            tt = new ThrowTest();
            try {
                tt.t1();                                   // Line 8
            }
            catch (Exception ex) {
                System.out.println("t1:");
                ex.printStackTrace(System.out);
            }
            try {
                tt.t2();                                   // Line 15
            }
            catch (Exception ex) {
                System.out.println("t2:");
                ex.printStackTrace(System.out);
            }
        }
    
        private static final Exception buildEx() {
            return new Exception("t1");                    // Line 24
        }
    
        private void t1() 
        throws Exception {
            throw buildEx();                               // Line 29
        }
    
        private void t2() 
        throws Exception {
            throw new Exception("t2");                     // Line 34
        }
    }
    
    $ java ThrowTest
    t1:
    java.lang.Exception: t1
        在 ThrowTest.buildEx(ThrowTest.java:24)
        在 ThrowTest.t1(ThrowTest.java:29)
        在 ThrowTest.main(ThrowTest.java:8)
    t2:
    java.lang.Exception: t2
        在 ThrowTest.t2(ThrowTest.java:34)
        在 ThrowTest.main(ThrowTest.java:15)

    【讨论】:

    • 我喜欢你的想法。您能否在代码中写下行号,以便在 SE 上易于理解堆栈跟踪?
    • 我认为第一个堆栈示例更好!
    • @JoshuaMN:更新使它更像您提议的机制(static 例外)。我更新了行号和构建器函数如何使堆栈跟踪更具可读性的示例。
    • 最后一个问题:你知道何时堆栈跟踪插入到异常吗?在创建期间 (new ..) ?我现在也想到了一个不同的问题:如果异常是可变的怎么办?现在这将是...... FUBAR。
    • @JoshuaMN:来自the docs“throwable 包含其线程在创建时的执行堆栈的快照。”Throwable 是异常的根源。)这就是上面代码的行为方式的原因,并且如果您预先创建异常,则不提供上下文。也就是说,您可以调用 fillInStackTrace 来覆盖它,但这意味着如果您预先创建,更多而不是更少的工作(并且一些例外不允许这样做)。
    【解决方案3】:

    另一个不这样做的原因是堆栈跟踪不合适。

    无论您在代码中的哪个位置抛出异常,在打印其堆栈跟踪时,它都会显示初始化异常的行而不是抛出异常的行(在这种特殊情况下,而不是预期Widget.fun() 堆栈跟踪将包含Widget.<clinit>)。

    因此您(或任何使用您的代码的人)将无法确定错误的实际位置。

    【讨论】:

      【解决方案4】:

      它认为在读取异常的堆栈跟踪时可能会导致问题。当代码抛出异常时堆栈跟踪被填充,但被存储到异常本身的实例中。如果您两次抛出相同的异常实例,我认为getStackTrace() 返回最后一个堆栈跟踪。如果由于某种原因(例如在多线程环境中)异常从代码中的不同位置抛出两次,然后打印出第一次抛出的堆栈跟踪可能是错误的。

      由于没有理由重复使用异常实例,我不建议您这样做。

      如果您想将异常用作一种包含附加信息(例如错误消息、错误代码等)的容器,请使用异常可序列化这一事实:在抛出之前克隆它。因此,每次您将抛出唯一的异常实例,但其字段将从预先创建的模板中复制。

      【讨论】:

        【解决方案5】:

        花哨的错误处理代码是不安全的。保持简单,保持明显,不要写任何你需要问问题的东西。错误处理不会引发额外的错误。

        【讨论】:

          【解决方案6】:

          我认为这是错误的,因为它会鼓励您使用异常来描述正常情况,而不仅仅是异常情况。请参阅 J. Bloch 撰写的 Effective Java 第二版,第 57 项,第 241 页。

          此外,您总是在堆中保留一个对象,所以这不是必需的,因为在现代 JVM 中对象创建非常快,而且一旦抛出异常,它可能会很快被垃圾回收。

          此外,您的代码可能会变得非常具有误导性,这会为一些非常困难的事情增加大量开销。

          【讨论】:

          • 请忽略编码风格。我需要实例来避免代码重复(我需要处理很多异常并抛出不同的异常)
          • @JoshuaMN 你应该在需要的地方抛出异常,如果你想你可以在一个方法中抽象出代码,如果你怀疑你的异常类型会改变,但一定要这样做这带有未经检查的异常,可以为您节省大量 catch/throws 子句的更改。如果抛出异常对您的应用程序来说是“正常”的事情,那么您应该考虑其他方法来修改程序流程而不是抛出异常,就像我说的应该只在异常情况下使用异常。
          猜你喜欢
          • 1970-01-01
          • 2017-08-05
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2022-06-24
          • 2021-02-07
          • 2013-01-23
          • 1970-01-01
          相关资源
          最近更新 更多