Why do this() and super() have to be the first statement in a constructor?
Java要求,如果在构造函数中调用这个()或Sub(),它必须是第一个语句。为什么?
例如:
1 2 3 4 5 6 7 8 9 10 | public class MyClass { public MyClass(int x) {} } public class MySubClass extends MyClass { public MySubClass(int a, int b) { int c = a + b; super(c); // COMPILE ERROR } } |
Sun编译器说"调用super必须是构造函数中的第一条语句"。Eclipse编译器说"构造函数调用必须是构造函数中的第一条语句"。
但是,您可以通过重新排列代码来绕过这个问题:
1 2 3 4 5 | public class MySubClass extends MyClass { public MySubClass(int a, int b) { super(a + b); // OK } } |
下面是另一个例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | public class MyClass { public MyClass(List list) {} } public class MySubClassA extends MyClass { public MySubClassA(Object item) { // Create a list that contains the item, and pass the list to super List list = new ArrayList(); list.add(item); super(list); // COMPILE ERROR } } public class MySubClassB extends MyClass { public MySubClassB(Object item) { // Create a list that contains the item, and pass the list to super super(Arrays.asList(new Object[] { item })); // OK } } |
所以,它不会阻止您在调用super之前执行逻辑。它只是阻止您执行无法放入单个表达式中的逻辑。
调用
为什么编译器有这些限制?你能给出一个代码示例,如果编译器没有这个限制,会发生什么不好的事情吗?
父类"EDOCX1"〔0〕需要在子类"EDOCX1"〔0〕之前调用。这将确保如果在构造函数中对父类调用任何方法,则父类已经正确设置。
您要做的是,将args传递给超级构造函数是完全合法的,您只需要像您所做的那样以内联方式构造这些args,或者将它们传递给构造函数,然后将它们传递给
1 2 3 4 5 |
如果编译器没有强制执行此操作,则可以执行此操作:
1 2 3 4 5 6 | public MySubClassB extends MyClass { public MySubClassB(Object[] myArray) { someMethodOnSuper(); //ERROR super not yet constructed super(myArray); } } |
如果一个
我通过链接构造函数和静态方法找到了解决这个问题的方法。我想做的事情是这样的:
1 2 3 4 5 6 7 8 9 10 11 12 |
因此,基本上基于构造函数参数构造一个对象,将该对象存储在一个成员中,并将该对象上方法的结果传递给super的构造函数。使成员成为最终成员也是相当重要的,因为类的性质是不可变的。注意,正如它所发生的那样,构造条实际上需要几个中间对象,所以在我的实际用例中,它不能简化为一行程序。
我最终使它像这样工作:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | public class Foo extends Baz { private final Bar myBar; private static Bar makeBar(String arg1, String arg2) { // My more complicated setup routine to actually make 'Bar' goes here... return new Bar(arg1, arg2); } public Foo(String arg1, String arg2) { this(makeBar(arg1, arg2)); } private Foo(Bar bar) { super(bar.baz()); myBar = bar; } } |
它完成了在调用超级构造函数之前执行多个语句的任务。
因为捷豹路虎是这么说的。是否可以以兼容的方式更改JLS以允许它?是的。但是,它会使语言规范复杂化,这已经足够复杂了。这不是一件非常有用的事情,而且有很多方法可以解决这一问题(使用方法
编辑:2018年3月:在消息记录:构造和验证中,Oracle建议删除此限制(但与C不同的是,在构造链接之前,
Historically, this() or super() must be first in a constructor. This
restriction was never popular, and perceived as arbitrary. There were
a number of subtle reasons, including the verification of
invokespecial, that contributed to this restriction. Over the years,
we've addressed these at the VM level, to the point where it becomes
practical to consider lifting this restriction, not just for records,
but for all constructors.
我相当肯定(熟悉Java规范的人),它是为了防止(a)允许使用部分构造的对象,以及(b)强制父类的构造函数在"新鲜"对象上构建。
一些"坏"的例子是:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | class Thing { final int x; Thing(int x) { this.x = x; } } class Bad1 extends Thing { final int z; Bad1(int x, int y) { this.z = this.x + this.y; // WHOOPS! x hasn't been set yet super(x); } } class Bad2 extends Thing { final int y; Bad2(int x, int y) { this.x = 33; this.y = y; super(x); // WHOOPS! x is supposed to be final } } |
因为这就是继承哲学。根据Java语言规范,构造函数的主体是如何定义的:
构造器正文:explicitConstructorInvocationoptblockStatementsopt
构造函数体的第一条语句可以是
- 显式调用同一类的另一个构造函数(通过使用关键字"this");或
- 直接超类的显式调用(通过使用关键字"super")。
如果构造函数主体不是以显式构造函数调用开始的,并且声明的构造函数不是原始类对象的一部分,则构造函数主体隐式地以超类构造函数调用"super();"开始,该调用是对其直接超类的不带参数的构造函数的调用。等等…将有一个完整的构造函数链被称为对象的构造函数,"Java平台中的所有类都是对象的子孙"。这就是所谓的"构造函数链接"。
这是为什么?< BR>Java以这种方式定义构造体的原因是,它们需要维护对象的层次结构。记住继承的定义;它正在扩展类。这么说,你就不能扩展不存在的东西。首先需要创建基(超类),然后可以派生它(子类)。这就是为什么他们称它们为父类和子类;没有父类就不能有子类。
在技术级别上,子类从其父类继承所有成员(字段、方法、嵌套类)。由于构造函数不是成员(它们不属于对象)。它们负责创建对象),因此它们不会被子类继承,但可以调用。因为在对象创建时只执行一个构造函数。那么,在创建子类对象时,我们如何保证超类的创建呢?因此,"构造函数链接"的概念;因此我们能够从当前构造函数中调用其他构造函数(即super)。而Java要求这个调用是子类构造函数中保持层次结构并保证它的第一行。他们假设如果你不先显式地创建父对象(比如你忘记了它),他们会隐式地为你创建父对象。
此检查在编译期间完成。但我不确定在运行时会发生什么,如果我们明确地试图在其中间的子类的构造函数中执行基本构造函数而不是从第一行执行基构造函数,则Java不会抛出编译错误。
你问为什么,其他的答案,我的意思是,不要说为什么打电话给你的超级建造师是可以的,但前提是这是第一行。原因是您没有真正调用构造函数。在C++中,等效语法是
1 2 3 4 5 6 7 8 9 | MySubClass: MyClass { public: MySubClass(int a, int b): MyClass(a+b) { } }; |
当你看到初始值设定项子句本身是那样的,在大括号之前,你知道它是特殊的。它在其他构造函数运行之前运行,实际上在初始化任何成员变量之前运行。对于Java来说,这并没有什么不同。有一种方法可以让一些代码(其他构造函数)在构造函数真正启动之前、子类的任何成员初始化之前运行。这样就可以把"呼叫"(如
如果,几行之后,它遇到一些代码说"哦,是的,当你构造这个对象的时候,这里有一些参数,我希望你传递给基类的构造函数",那就太晚了,没有任何意义。所以你会得到一个编译器错误。
So, it is not stopping you from executing logic before the call to
super. It is just stopping you from executing logic that you can't fit
into a single expression.
实际上,您可以用几个开销来执行逻辑,只需将代码包装在一个静态函数中,并在super语句中调用它。
使用您的示例:
1 2 3 4 5 6 7 8 9 10 11 12 |
我完全同意,限制太强了。使用静态helper方法(如tom hawtin-tockine建议的)或将所有"pre-super()计算"推送到参数中的单个表达式中并不总是可能的,例如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | class Sup { public Sup(final int x_) { //cheap constructor } public Sup(final Sup sup_) { //expensive copy constructor } } class Sub extends Sup { private int x; public Sub(final Sub aSub) { /* for aSub with aSub.x == 0, * the expensive copy constructor is unnecessary: */ /* if (aSub.x == 0) { * super(0); * } else { * super(aSub); * } * above gives error since if-construct before super() is not allowed. */ /* super((aSub.x == 0) ? 0 : aSub); * above gives error since the ?-operator's type is Object */ super(aSub); // much slower :( // further initialization of aSub } } |
如Carson-Myers建议的那样,使用"尚未构造的对象"例外情况会有所帮助,但在每个对象构造期间检查这一点会减慢执行速度。我希望Java编译器能够更好地区分(而不是不允许禁止if语句,而允许)。-参数中的运算符),即使这会使语言规范复杂化。
我猜他们是这样做的,为编写Java代码的工具的人们提供了更轻松的生活,在某种程度上,他们也在阅读Java代码。
如果允许
让每个人做这些额外的工作似乎是一个比利益更大的成本。
Can you give a code example where, if the compiler did not have this restriction, something bad would happen?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | class Good { int essential1; int essential2; Good(int n) { if (n > 100) throw new IllegalArgumentException("n is too large!"); essential1 = 1 / n; essential2 = n + 2; } } class Bad extends Good { Bad(int n) { try { super(n); } catch (Exception e) { // Exception is ignored } } public static void main(String[] args) { Bad b = new Bad(0); // b = new Bad(101); System.out.println(b.essential1 + b.essential2); } } |
构造过程中的一个异常几乎总是表示正在构造的对象无法正确初始化,现在处于错误状态,不可用,必须进行垃圾收集。但是,子类的构造函数能够忽略它的一个超类中发生的异常并返回一个部分初始化的对象。在上面的例子中,如果给
你可以说忽略例外总是个坏主意。好的,下面是另一个例子:
1 2 3 4 5 6 | class Bad extends Good { Bad(int n) { for (int i = 0; i < n; i++) super(i); } } |
很有趣,不是吗?在这个例子中,我们要创建多少对象?一个?两个?或者什么都没有…
允许在构造函数的中间调用
另一方面,我理解在调用
在这种情况下,您有以下机会:
1 | return this; |
最后。以及编译器拒绝代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | public int get() { int x; for (int i = 0; i < 10; i++) x = i; return x; } public int get(int y) { int x; if (y > 0) x = y; return x; } public int get(boolean b) { int x; try { x = 1; } catch (Exception e) { } return x; } |
由于错误"变量x可能尚未初始化",它可以对
我发现了一个麻烦。
这不会编译:
1 2 3 4 5 6 7 8 9 | public class MySubClass extends MyClass { public MySubClass(int a, int b) { int c = a + b; super(c); // COMPILE ERROR doSomething(c); doSomething2(a); doSomething3(b); } } |
这工作:
1 2 3 4 5 6 7 8 9 10 11 12 | public class MySubClass extends MyClass { public MySubClass(int a, int b) { this(a + b); doSomething2(a); doSomething3(b); } private MySubClass(int c) { super(c); doSomething(c); } } |
It makes sense that constructors complete their execution in order of
derivation. Because a superclass has no knowledge of any subclass, any
initialization it needs to perform is separate from and possibly
prerequisite to any initialization performed by the subclass.
Therefore, it must complete its execution first.
简单演示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | class A { A() { System.out.println("Inside A's constructor."); } } class B extends A { B() { System.out.println("Inside B's constructor."); } } class C extends B { C() { System.out.println("Inside C's constructor."); } } class CallingCons { public static void main(String args[]) { C c = new C(); } } |
此程序的输出是:
1 2 3 | Inside A's constructor Inside B's constructor Inside C's constructor |
在调用子级的构造函数之前,可以使用匿名初始值设定项块初始化子级中的字段。此示例将演示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
这将输出:
In parent
In initializer
In child
这是因为您的构造函数依赖于其他构造函数。对于您的构造函数来说,它是正确工作的必要条件,而其他构造函数则是正确工作的,这是相互依赖的。这就是为什么必须首先检查依赖构造函数的原因,依赖构造函数由构造函数中的this()或super()调用。如果此()或super()调用的其他构造函数有问题,那么什么时候执行其他语句,因为如果调用的构造函数失败,所有语句都将失败。
我知道我参加聚会有点晚了,但我已经用过几次这个技巧(我知道这有点不寻常):
我用一种方法创建了一个通用接口
1 |
如果在将它传递给构造函数之前我需要做一些事情,那么我只需要这样做:
1 2 3 4 5 | super(new InfoRunnable<ThingToPass>() { public ThingToPass run(Object... args) { /* do your things here */ } }.run(/* args here */)); |
实际上,
在构造子对象之前,必须先创建父对象。正如你所知道的,当你写这样的类时:
它转到下一个(扩展和超级只是隐藏):
1 2 3 4 5 6 |
首先我们创建一个
那么为什么我们不能在任何方法之后执行
1 2 3 4 5 6 7 8 9 10 |
正如其他人所说,您可以执行这样的代码:
1 | this(a+b); |
您还可以执行如下代码:
1 2 3 | public MyClass(int a, SomeObject someObject) { this(someObject.add(a+5)); } |
但是您不能执行这样的代码,因为您的方法还不存在:
1 2 3 4 5 6 7 8 9 10 11 | public MyClass extends Object{ public MyClass(int a) { } public MyClass(int a, int b) { this(add(a, b)); } public int add(int a, int b){ return a+b; } } |
此外,您还必须在您的EDOCX1方法链中有
1 2 3 4 5 6 7 8 | public MyClass{ public MyClass(int a) { this(a, 5); } public MyClass(int a, int b) { this(a); } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
例如,如果我们调用构造函数
Tldr:
其他的答案解决了这个问题的"为什么"。我将提供一个关于这个限制的黑客程序:
基本思想是用嵌入的语句劫持
考虑到我们在调用
1 2 3 4 5 6 7 8 9 | public class Child extends Parent { public Child(T1 _1, T2 _2, T3 _3) { Statement_1(); Statement_2(); Statement_3(); // and etc... Statement_9(); super(_1, _2, _3); // compiler rejects because this is not the first line } } |
编译器当然会拒绝我们的代码。因此,我们可以这样做:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | // This compiles fine: public class Child extends Parent { public Child(T1 _1, T2 _2, T3 _3) { super(F(_1), _2, _3); } public static T1 F(T1 _1) { Statement_1(); Statement_2(); Statement_3(); // and etc... Statement_9(); return _1; } } |
唯一的限制是父类必须有一个构造函数,它至少接受一个参数,这样我们就可以将语句作为表达式偷偷地输入。
下面是一个更详细的例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | public class Child extends Parent { public Child(int i, String s, T1 t1) { i = i * 10 - 123; if (s.length() > i) { s ="This is substr s:" + s.substring(0, 5); } else { s ="Asdfg"; } t1.Set(i); T2 t2 = t1.Get(); t2.F(); Object obj = Static_Class.A_Static_Method(i, s, t1); super(obj, i,"some argument", s, t1, t2); // compiler rejects because this is not the first line } } |
重新加工成:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | // This compiles fine: public class Child extends Parent { public Child(int i, String s, T1 t1) { super(Arg1(i, s, t1), Arg2(i),"some argument", Arg4(i, s), t1, Arg6(i, t1)); } private static Object Arg1(int i, String s, T1 t1) { i = Arg2(i); s = Arg4(s); return Static_Class.A_Static_Method(i, s, t1); } private static int Arg2(int i) { i = i * 10 - 123; return i; } private static String Arg4(int i, String s) { i = Arg2(i); if (s.length() > i) { s ="This is sub s:" + s.substring(0, 5); } else { s ="Asdfg"; } return s; } private static T2 Arg6(int i, T1 t1) { i = Arg2(i); t1.Set(i); T2 t2 = t1.Get(); t2.F(); return t2; } } |
事实上,编译器可以为我们实现这个过程的自动化。他们只是选择了不去。