【问题标题】:Why can't I use a try block around my super() call?为什么我不能在我的 super() 调用周围使用 try 块?
【发布时间】:2010-09-05 12:59:41
【问题描述】:

因此,在 Java 中,构造函数的第一行必须是对 super 的调用……无论是隐式调用 super(),还是显式调用另一个构造函数。我想知道的是,为什么我不能在它周围放一个 try 块?

我的具体情况是我有一个模拟类进行测试。没有默认构造函数,但我想要一个使测试更易于阅读。我还想将构造函数抛出的异常包装到 RuntimeException 中。

所以,我实际上想要做的是:

public class MyClassMock extends MyClass {
    public MyClassMock() {
        try {
            super(0);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    // Mocked methods
}

但是 Java 抱怨 super 不是第一个语句。

我的解决方法:

public class MyClassMock extends MyClass {
    public static MyClassMock construct() {
        try {
            return new MyClassMock();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    public MyClassMock() throws Exception {
        super(0);
    }

    // Mocked methods
}

这是最好的解决方法吗?为什么 Java 不让我做前者?


对于“为什么”,我最好的猜测是 Java 不希望我有一个处于潜在不一致状态的构造对象......但是,在进行模拟时,我不在乎。看来我应该能够做到以上...或者至少我知道以上对于我的情况是安全的...或者似乎无论如何都应该这样做。

我正在覆盖我在测试类中使用的任何方法,因此我没有使用未初始化变量的风险。

【问题讨论】:

  • 一个有趣的提示是,这纯粹是 Java 语言的限制。等效的字节码是完全有效的。
  • 您确定字节码仍然有效吗?我记得在有人利用了我在下面演示的由此产生的安全漏洞后,它就失效了。
  • 因为规则不允许。阅读JDK spec。即使你通过了编译器,验证器也会拒绝它。

标签: java exception mocking try-catch


【解决方案1】:

不幸的是,编译器无法根据理论原理工作,即使您可能知道它在您的情况下是安全的,但如果他们允许,它必须在所有情况下都是安全的。

换句话说,编译器不仅会阻止您,还会阻止所有人,包括所有不知道它不安全且需要特殊处理的人。这可能还有其他原因,因为如果知道如何处理它们,所有语言通常都有办法做不安全的事情。

在 C# .NET 中也有类似的规定,声明一个调用基础构造函数的构造函数的唯一方法是:

public ClassName(...) : base(...)

这样做时,基础构造函数将在构造函数主体之前被调用,并且您无法更改此顺序。

【讨论】:

  • 为什么不阻止你在 catch 块中使用它呢?这涵盖了包装异常的常见情况。
【解决方案2】:

我不知道 Java 内部是如何实现的,但是如果超类的构造函数抛出异常,那么就没有你扩展的类的实例。例如,不可能调用 toString()equals() 方法,因为它们在大多数情况下都是继承的。

Java 可能允许在构造函数中对 super() 调用进行 try/catch,如果 1. 您覆盖了超类中的所有方法,并且 2. 您不使用 super.XXX() 子句,但这一切听起来对我来说太复杂了。

【讨论】:

    【解决方案3】:

    我不能假设对 Java 内部有深入的了解,但我的理解是,当编译器需要实例化派生类时,它必须首先创建基类(以及在此之前的基类(.. .)),然后在子类中进行扩展。

    因此,即使是未初始化的变量或类似的东西也不会有危险。当您尝试在子类的构造函数中基类的构造函数构造函数中做某事时,您基本上是在要求编译器扩展一个尚不存在的基对象实例.

    编辑:在您的情况下,MyClass 成为基础对象,而 MyClassMock 是子类。

    【讨论】:

      【解决方案4】:

      这样做是为了防止有人从不受信任的代码中创建新的SecurityManager 对象。

      public class Evil : SecurityManager {
        Evil()
        {
            try {
               super();
            } catch { Throwable t }
            {
            }
         }
      }
      

      【讨论】:

        【解决方案5】:

        我知道这是一个老问题,但我喜欢它,因此,我决定自己给出一个答案。也许我对为什么不能这样做的理解将有助于讨论和您有趣问题的未来读者。

        让我从一个对象构造失败的例子开始。

        让我们定义一个类 A,这样:

        class A {
           private String a = "A";
        
           public A() throws Exception {
                throw new Exception();
           }
        }
        

        现在,假设我们想在 try...catch 块中创建 A 类型的对象。

        A a = null;
        try{
          a = new A();
        }catch(Exception e) {
          //...
        }
        System.out.println(a);
        

        显然,这段代码的输出将是:null

        为什么 Java 不返回 A 的部分构造版本?毕竟到构造函数失败的时候,对象的name字段已经被初始化了对吧?

        好吧,Java 无法返回 A 的部分构造版本,因为该对象未成功构建。该对象处于不一致状态,因此被 Java 丢弃。您的变量 A 甚至没有初始化,它保持为空。

        现在,如您所知,要完全构建一个新对象,必须首先初始化它的所有超类。如果其中一个超类未能执行,那么该对象的最终状态是什么?无法确定。

        看看这个更详细的例子

        class A {
           private final int a;
           public A() throws Exception { 
              a = 10;
           }
        }
        
        class B extends A {
           private final int b;
           public B() throws Exception {
               methodThatThrowsException(); 
               b = 20;
           }
        }
        
        class C extends B {
           public C() throws Exception { super(); }
        }
        

        C的构造函数被调用时,如果在初始化B时发生异常,那么最终的int变量b的值是多少?

        因此,无法创建对象 C,它是伪造的,是垃圾,没有完全初始化。

        对我来说,这解释了为什么你的代码是非法的。

        【讨论】:

          【解决方案6】:

          绕过它的一种方法是调用私有静态函数。然后可以将 try-catch 放入函数体中。

          public class Test  {
            public Test()  {
               this(Test.getObjectThatMightThrowException());
            }
            public Test(Object o)  {
               //...
            }
            private static final Object getObjectThatMightThrowException()  {
               try  {
                  return  new ObjectThatMightThrowAnException();
               }  catch(RuntimeException rtx)  {
                  throw  new RuntimeException("It threw an exception!!!", rtx);
               }
            }
          }
          

          【讨论】:

          • 想详细说明为什么?
          • 我详细阐述了一点。够了吗?
          • 为什么在这里调用私有静态函数有效?以及您认为不起作用的 OP 代码有什么问题?
          • 关于继承的问题,在这段代码中有组合
          【解决方案7】:

          我知道这个问题有很多答案,但我想谈谈为什么不允许这样做,特别是回答为什么 Java 不允许你这样做。所以你去...

          现在,请记住 super() 必须在子类的构造函数中的任何其他内容之前调用,因此,如果您确实在 super() 调用周围使用了 trycatch 块,则这些块必须看起来像这样:

          try {
             super();
             ...
          } catch (Exception e) {
             super(); //This line will throw the same error...
             ...
          }
          

          如果super()try 块中失败,它必须首先在catch 块中执行,以便super 在子类构造函数中的任何内容之前运行。这给您留下了与开始时相同的问题:如果抛出异常,则不会被捕获。 (在这种情况下,它只是在 catch 块中再次被抛出。)

          现在,Java 也不允许使用上述代码。这段代码可能会执行第一次超级调用的一半,然后再次调用,这可能会导致某些超级类出现问题。

          现在,Java 不允许您抛出异常而不是调用super() 的原因是异常可能在其他地方被捕获,并且程序将继续不调用super() 在您的子类对象上, 并且可能是因为异常可能会将您的对象作为参数并尝试更改尚未初始化的继承实例变量的值。

          【讨论】:

          • 执行try { super(); ... } catch (SomeException e) { throw new SomeOtherException(e); } 会很有用,因此允许构造函数具有完全不同的异常说明符。
          猜你喜欢
          • 1970-01-01
          • 2011-06-08
          • 1970-01-01
          • 1970-01-01
          • 2013-12-12
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多