为什么我不能在 super() 调用周围使用 try 块?
题
因此,在 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 不想让我有一个处于潜在不一致状态的构造对象......然而,在进行模拟时,我并不关心这一点。看来我应该能够做到以上...或者至少我知道上述内容对于我的情况是安全的......或者看起来无论如何都应该如此。
我将覆盖测试类中使用的任何方法,因此不存在使用未初始化变量的风险。
解决方案
不幸的是,编译器无法按照理论原则工作,即使您可能知道它在您的情况下是安全的,如果他们允许的话,它对于所有情况都必须是安全的。
换句话说,编译器不仅仅阻止你,它阻止所有人,包括所有那些不知道它不安全并且需要特殊处理的人。可能还有其他原因,因为所有语言通常都有办法做到这一点 不安全 事情,如果一个人知道如何处理它们。
在 C# .NET 中也有类似的规定,声明调用基构造函数的构造函数的唯一方法是:
public ClassName(...) : base(...)
这样做时,将在构造函数主体之前调用基本构造函数,并且您无法更改此顺序。
其他提示
这样做是为了防止有人创建新的 SecurityManager
来自不可信代码的对象。
public class Evil : SecurityManager {
Evil()
{
try {
super();
} catch { Throwable t }
{
}
}
}
我知道这是一个老问题,但我喜欢它,因此,我决定给出自己的答案。也许我对为什么不能这样做的理解将有助于讨论和您有趣问题的未来读者。
让我从一个失败的对象构造的例子开始。
让我们定义一个类 A,这样:
class A {
private String a = "A";
public A() throws Exception {
throw new Exception();
}
}
现在,假设我们想在 a 中创建一个 A 类型的对象 try...catch
堵塞。
A a = null;
try{
a = new A();
}catch(Exception e) {
//...
}
System.out.println(a);
显然,这段代码的输出将是: null
.
为什么 Java 不返回部分构造的版本 A
?毕竟,当构造函数失败时,对象的 name
字段已经初始化了,对吗?
好吧,Java 无法返回部分构造的版本 A
因为对象没有成功构建。该对象处于不一致状态,因此被 Java 丢弃。您的变量 A 甚至没有初始化,它保持为 null。
现在,如您所知,要完全构建一个新对象,必须首先初始化其所有超类。如果其中一个超类未能执行,该对象的最终状态是什么?这是不可能确定的。
看看这个更详细的例子
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 无法被创建,它是假的,它是垃圾,它没有完全初始化。
对我来说,这解释了为什么你的代码是非法的。
我不能假设对 Java 内部结构有深入的了解,但我的理解是,当编译器需要实例化派生类时,它必须首先创建基类(以及在此之前的基类(...))然后在子类中进行扩展。
因此,这根本不是未初始化变量或类似内容的危险。当您尝试在子类的构造函数中执行某些操作时 前 基类' 构造函数, ,您基本上是在要求编译器扩展尚不存在的基础对象实例。
编辑:就你而言, 我的课 成为基础对象,并且 我的类模拟 是一个子类。
我不知道Java内部是如何实现的,但是如果超类的构造函数抛出异常,那么就没有你扩展的类的实例。不可能打电话给 toString()
或者 equals()
例如,方法,因为它们在大多数情况下是继承的。
如果为 1,Java 可能允许在构造函数中对 super() 调用进行 try/catch。您重写超类中的所有方法,并且 2.你不使用 super.XXX() 子句,但这对我来说听起来太复杂了。
我知道这个问题有很多答案,但我想谈谈为什么不允许这样做,特别是回答为什么 Java 不允许你这样做。所以你来吧...
现在,请记住 super()
必须在子类的构造函数中的任何其他内容之前调用,因此,如果您确实使用 try
和 catch
你周围的街区 super()
调用时,块必须如下所示:
try {
super();
...
} catch (Exception e) {
super(); //This line will throw the same error...
...
}
如果超级()fails in the
尝试block, it HAS to be executed first in the
抓住block, so that
极好的runs before anything in your subclass
s 构造函数。这会让你遇到与开始时相同的问题:如果抛出异常,则不会捕获该异常。(在这种情况下,它只是再次被抛出到 catch 块中。)
现在,上述代码也绝不是 Java 所允许的。这段代码可能会执行第一个超级调用的一半,然后再次调用它,这可能会导致某些超级类出现问题。
现在,Java 不允许抛出异常的原因 反而 的呼唤 super()
是因为异常可能在其他地方被捕获,并且程序将继续 不打电话 super()
在你的子类对象上, 可能是因为异常可能会将您的对象作为参数并尝试更改继承的实例变量的值,而该变量尚未初始化。
解决这个问题的一种方法是调用私有静态函数。然后可以将 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);
}
}
}