当前位置: 代码迷 >> 综合 >> 为什么this()和super()必须是构造函数中的第一条语句?
  详细解决方案

为什么this()和super()必须是构造函数中的第一条语句?

热度:40   发布时间:2024-01-24 19:35:46.0

Java要求,如果您在构造函数中调用this()或super(),则它必须是第一条语句。 为什么?

例如:

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编译器说“构造函数调用必须是构造函数中的第一条语句”。

但是,您可以通过重新安排一些代码来解决此问题:

public class MySubClass extends MyClass {public MySubClass(int a, int b) {super(a + b);  // OK}
}

这是另一个示例:

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 superList 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 supersuper(Arrays.asList(new Object[] { item }));  // OK}
}

因此,这不会阻止您在调用super之前执行逻辑 。 这只是在阻止您执行无法包含在单个表达式中的逻辑。

调用this()有类似的规则。 编译器说“对此的调用必须是构造函数中的第一条语句”。

为什么编译器有这些限制? 您能否举一个代码示例,如果编译器没有此限制,那么会发生不好的事情吗?


#1楼

父类的constructor需要在子类的constructor之前调用。 这将确保如果您在构造函数中的父类上调用任何方法,则该父类已经正确设置。

您想要做的是,将args传递给super构造函数是完全合法的,您只需要在执行操作时内联构造这些args,或者将它们传递给构造函数,然后将它们传递给super :

public MySubClassB extends MyClass {public MySubClassB(Object[] myArray) {super(myArray);}
}

如果编译器未强制执行此操作,则可以执行以下操作:

public MySubClassB extends MyClass {public MySubClassB(Object[] myArray) {someMethodOnSuper(); //ERROR super not yet constructedsuper(myArray);}
}

如果parent类具有默认构造函数,则compiler会自动为您插入对super的调用。 由于Java中的每个类都继承自Object ,因此必须以某种方式调用对象构造函数,并且必须首先执行它。 编译器自动插入super()可以实现这一点。 强制super出现在最前面,强制以正确的顺序执行构造函数主体,即:对象->父->子-> ChildOfChild-> SoOnSoForth


#2楼

我相当确定(熟悉Java规范的人),这是为了防止您(a)被允许使用部分构造的对象,以及(b)强制父类的构造函数以“新鲜”构造”对象。

一些“坏”事情的例子是:

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 yetsuper(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}        
}

#3楼

您可以在调用子级的构造函数之前,使用匿名的初始化程序块来初始化子级中的字段。 此示例将演示:

public class Test {public static void main(String[] args) {new Child();}
}class Parent {public Parent() {System.out.println("In parent");}
}class Child extends Parent {{System.out.println("In initializer");}public Child() {super();System.out.println("In child");}
}

这将输出:

在父母中
在初始化器中
在儿童中


#4楼

因为JLS是这样说的。 是否可以以兼容的方式更改JLS以允许它? 对。

但是,这会使语言规范变得复杂,这已经不够复杂了。 这样做并不是一件非常有用的事情,并且有很多解决方法(用静态方法或lambda表达式的结果this(fn())调用另一个构造函数-该方法在另一个构造函数之前被调用,因此也超级构造函数)。 因此,进行更改的功率重量比是不利的。

请注意,仅此规则并不能防止在超类完成构造之前使用字段。

考虑这些非法的例子。

super(this.x = 5);super(this.fn());super(fn());super(x);super(this instanceof SubClass);
// this.getClass() would be /really/ useful sometimes.

这个例子是合法的,但是是“错误的”。

class MyBase {MyBase() {fn();}abstract void fn();
}
class MyDerived extends MyBase {void fn() {// ???}
}

在上面的示例中,如果MyDerived.fn需要MyDerived构造函数的参数,则需要使用ThreadLocal将它们隐藏起来。 ;(

顺便提及,由于Java 1.4中,包含外部合成领域this是内部类超级构造函数被调用之前分配。 这在为目标早期版本而编译的代码中引起了特殊的NullPointerException事件。

另请注意,在存在不安全的出版物的情况下,除非采取预防措施,否则可以通过其他线程重新查看构造。

编辑2018年3月:在消息记录:建筑和验证甲骨文是在暗示这个限制被删除(但不像C#, this将是构造函数链之前肯定未分配 (DU))。

从历史上看,this()或super()必须首先在构造函数中。 这种限制从来没有流行过,并且被认为是任意的。 造成这种限制的原因有很多微妙的原因,包括对invokespecial的验证。 多年来,我们已经在虚拟机级别解决了这些问题,以至于考虑解除此限制不仅对于记录,而且对于所有构造函数都是可行的。


#5楼

仅仅是因为这是继承哲学。 并且根据Java语言规范,这是定义构造函数主体的方式:

ConstructorBody:{ExplicitConstructorInvocation opt BlockStatements opt }

构造函数主体的第一条语句可以是

  • 显式调用同一类的另一个构造函数(通过使用关键字“ this”); 要么
  • 直接超类的显式调用(通过使用关键字“ super”)

如果构造函数主体不是以显式构造函数调用开始的,并且声明的构造函数不是原始类Object的一部分,则构造函数主体将隐式以超类构造函数调用“ super();”开头,该超类构造函数调用为它的直接超类,不带参数。 依此类推..将有整个构造函数链,一直追溯到Object的构造函数; “ Java平台中的所有类都是对象的后代”。 这件事称为“ 构造函数链接 ”。

现在为什么呢?
Java之所以以这种方式定义ConstructorBody的原因是,他们需要维护对象的层次结构 。 记住继承的定义; 它扩展了课程。 话虽这么说,您不能扩展不存在的东西。 首先需要创建基(超类),然后可以派生它(子类)。 这就是为什么他们称它们为“父母和孩子”类; 你不能没有父母就生孩子。

从技术上讲,子类从其父类继承所有成员(字段,方法,嵌套类)。 而且由于构造函数不是成员(它们不属于对象。它们负责创建对象),因此它们不会被子类继承,但是可以调用它们。 并且由于在创建对象时仅执行一个构造函数 。 那么,当您创建子类对象时,如何保证创建超类呢? 因此,“构造链接”的概念; 因此,我们可以从当前构造函数中调用其他构造函数(即super)。 Java要求此调用是子类构造函数中的FIRST行,以维护层次结构并保证层次结构。 他们假设,如果您不显式创建父对象FIRST(就像您忘记了它一样),他们将为您隐式地创建它。

此检查在编译期间完成。 但是我不确定在运行时会发生什么,我们会得到什么样的运行时错误,当我们明确尝试从子类的构造函数内部的基本构造函数中执行基本构造函数时,如果Java没有抛出编译错误,身体,而不是从第一行...


#6楼

我的猜测是,这样做是为了使编写处理Java代码的工具的人员和阅读Java代码的人员在某种程度上更轻松。

如果允许super()或this()调用移动,则有更多变化需要检查。 例如,如果将super()或this()调用移至条件if()则可能必须足够聪明才能将隐式super()插入else 。 如果您两次调用super()或一起使用super()和this() ,则可能需要知道如何报告错误。 可能需要禁止在接收器上进行方法调用,直到调用super()或this()为止,并弄清何时变得复杂。

让每个人都做这些额外的工作似乎比付出的代价更大。


#7楼

因此,这不会阻止您在调用super之前执行逻辑。 这只是在阻止您执行无法包含在单个表达式中的逻辑。

实际上,您可以使用多种方法执行逻辑,只需要将代码包装在静态函数中,然后在super语句中调用它即可。

使用您的示例:

public class MySubClassC extends MyClass {public MySubClassC(Object item) {// Create a list that contains the item, and pass the list to supersuper(createList(item));  // OK}private static List createList(item) {List list = new ArrayList();list.add(item);return list;}
}

#8楼

Tldr:

其他答案解决了问题的“原因”。 我将针对此限制提供一些技巧 :

基本思想是用您的嵌入式语句劫持 super语句。 这可以通过将语句伪装为表达式来完成。

Tsdr:

考虑我们要在调用super()之前对Statement9()执行Statement1() super() :

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}
}

编译器当然会拒绝我们的代码。 因此,我们可以这样做:

// 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;}
}

唯一的限制是, 父类必须具有一个至少包含一个参数的构造函数,以便我们可以将表达式作为表达式潜入。

这是一个更详细的示例:

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}
}

重做为:

// 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;}
}

实际上,编译器可以为我们自动化该过程。 他们只是选择不这样做。


#9楼

我找到了解决方法。

这不会编译:

public class MySubClass extends MyClass {public MySubClass(int a, int b) {int c = a + b;super(c);  // COMPILE ERRORdoSomething(c);doSomething2(a);doSomething3(b);}
}

这有效:

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);}
}

#10楼

我知道我参加聚会有点晚了,但是我已经使用了几次这个技巧(而且我知道这有点不寻常):

我使用一种方法创建通用接口InfoRunnable<T> :

public T run(Object... args);

如果需要在将其传递给构造函数之前做一些事情,请执行以下操作:

super(new InfoRunnable<ThingToPass>() {public ThingToPass run(Object... args) {/* do your things here */}
}.run(/* args here */));

#11楼

在构造子对象之前,必须先创建父对象。 如您所知,当您编写这样的类时:

public MyClass {public MyClass(String someArg) {System.out.println(someArg);}
}

转到下一个(扩展和超级隐藏):

public MyClass extends Object{public MyClass(String someArg) {super();System.out.println(someArg);}
}

首先,我们创建一个Object ,然后将该对象扩展到MyClass 。 我们不能在Object之前创建MyClass 。 一个简单的规则是,必须在子构造函数之前调用父构造函数。 但是我们知道,类可以具有一个以上的构造函数。 Java允许我们选择一个将被调用的构造函数(它将是super()或super(yourArgs...) )。 因此,当您编写super(yourArgs...)您将重新定义构造函数,该构造函数将被调用以创建父对象。 您无法在super()之前执行其他方法,因为该对象尚不存在(但是在super() ,将创建一个对象,您将可以执行所需的任何操作)。

那么,为什么不能在任何方法之后执行this()呢? 如您所知, this()是当前类的构造函数。 同样,我们的类中可以有不同数量的构造函数,并像this()或this(yourArgs...)那样调用它们。 如我所说,每个构造函数都有隐藏的方法super() 。 当我们编写自定义的super(yourArgs...)时,我们用super(yourArgs...)删除了super() super(yourArgs...) 。 同样,当我们定义this()或this(yourArgs...)我们也会在当前构造函数中删除我们的super() ,因为如果super()与this()在同一方法中一起使用,它将创建一个以上的父对象。 这就是为什么对this()方法施加相同规则的原因。 它只是将父对象的创建传递给另一个子构造函数,该构造函数调用super()构造函数进行父创建。 因此,代码实际上将是这样的:

public MyClass extends Object{public MyClass(int a) {super();System.out.println(a);}public MyClass(int a, int b) {this(a);System.out.println(b);}
}

正如其他人所说,您可以执行以下代码:

this(a+b);

您也可以执行如下代码:

public MyClass(int a, SomeObject someObject) {this(someObject.add(a+5));
}

但是您不能执行这样的代码,因为您的方法尚不存在:

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;}
}

另外,您必须在this()方法链中具有super()构造函数。 您不能像这样创建对象:

public MyClass{public MyClass(int a) {this(a, 5);}public MyClass(int a, int b) {this(a);}
}

#12楼

class C
{int y,z;C(){y=10;}C(int x){C();z=x+y;System.out.println(z);}
}class A
{public static void main(String a[]){new C(10);}
}

参见示例,如果我们正在调用构造函数C(int x)则z的值取决于y,如果我们在第一行中未调用C() ,则z将会出现问题。 z将无法获得正确的值。


#13楼

构造函数按派生顺序完成其执行是有意义的。 因为超类不了解任何子类,所以它需要执行的任何初始化与子类执行的任何初始化是分离的,并且可能是前提。 因此,它必须首先完成其执行。

一个简单的演示:

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();}
}

该程序的输出为:

Inside A's constructor
Inside B's constructor
Inside C's constructor

#14楼

实际上, super()是构造函数的第一条语句,因为要确保其超类在构造子类之前已完全形成。 即使您的第一条语句中没有super() ,编译器也会为您添加它!


#15楼

您能否举一个代码示例,如果编译器没有此限制,那么会发生不好的事情吗?

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);}
}

构造期间的异常几乎总是表示正在构造的对象无法正确初始化,现在处于错误状态,无法使用,并且必须进行垃圾回收。 但是,子类的构造函数可以忽略在其父类之一中发生的异常并返回部分初始化的对象。 在上面的示例中,如果提供给new Bad()的参数new Bad() 0或大于100,则essential1和essential2都不会正确初始化。

您可能会说忽略异常总是一个坏主意。 好,这是另一个例子:

class Bad extends Good {Bad(int n) {for (int i = 0; i < n; i++)super(i);}
}

好笑,不是吗? 在此示例中,我们要创建多少个对象? 一? 二? 也许什么都没有...

允许在构造函数的中间调用super()或this()将打开潘多拉盒子的令人讨厌的构造函数。


另一方面,我知道在调用super()或this()之前经常需要包含一些静态部分。 这可能是任何不依赖this引用的代码(实际上, this引用已经存在于构造函数的最开始,但是在super()或this()返回之前无法有序使用)并且需要进行此类调用。 另外,像在任何方法中一样,有可能在调用super()或this()之前需要创建一些局部变量。

在这种情况下,您有以下机会:

  1. 使用此答案中显示的模式,可以避免这种限制。
  2. 等待Java团队允许pre- super()和pre- this()代码。 这可以通过对super()或this()在构造函数中可能出现的位置施加限制来完成。 实际上,即使是今天的编译器,也能够以足以安全地允许在构造函数开始时添加静态代码的程度来区分好和坏(或潜在的坏)情况。 确实,假设super()和this()返回this引用,然后,构造函数将

return this;

在末尾。 以及编译器拒绝代码

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可能尚未初始化”,它可以this变量执行this ,就像对其他任何局部变量一样对其进行检查。 唯一的区别是this不能由比其它的任何方法来指定super()或this()调用(和往常一样,如果在一个构造没有这样的呼叫, super()被隐含由编译器在开始插入的)和可能不会分配两次。 如有任何疑问(例如在第一个get() ,实际上x总是被分配),编译器可能会返回错误。 这比在任何在super()或this()之前除了注释之外的构造函数上简单地返回错误要好。


#16楼

那是因为您的构造函数依赖于其他构造函数。 为了使您的构造函数正常工作,它对其他依赖于其他构造函数的正常工作必不可少。 这就是为什么必须首先检查依赖构造函数的原因,该构造函数在构造函数中由this()或super()调用。 如果其他通过this()或super()调用的构造函数有问题,那么执行其他语句是什么,因为如果调用的构造函数失败,所有语句都会失败。


#17楼

我完全同意,限制太严格了。 使用静态辅助方法(如Tom Hawtin-粘性线建议)或将所有“ pre-super()计算”推入参数中的单个表达式并不总是可能的,例如:

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语句,但允许在参数中使用?运算符),即使这会使语言规范变得复杂。


#18楼

您问为什么,其他答案imo并没有真正说出为什么可以调用您的super的构造函数,但前提是它是第一行。 原因是您没有真正调用构造函数。 在C ++中,等效语法为

MySubClass: MyClass {public:MySubClass(int a, int b): MyClass(a+b){}};

当您这样看到初始化器子句时,在大括号之前,您会知道它很特殊。 它在构造函数的其余部分运行之前运行,实际上在任何成员变量初始化之前运行。 对于Java来说并没有什么不同。 有一种方法可以让一些代码(其他构造函数)在构造函数真正开始之前,子类的任何成员初始化之前运行。 那样的话就是把“ call”(例如super )放在第一行。 (从某种意义上说, super或this有点在第一个开括号之前,即使您在后面键入,因为它会在您完全构建完所有东西之前执行。) (例如int c = a + b; )使编译器说“哦,好的,没有其他构造函数,那么我们就可以初始化所有内容。” 因此它开始运行,并初始化您的超类,成员和诸如此类的东西,然后在大括号之后开始执行代码。

如果几行后,它遇到一些代码,说“哦,是的,当您构造此对象时,这是我希望您传递给基类的构造函数的参数”,这为时已晚,并且没有有任何道理。 因此,您会收到一个编译器错误。


#19楼

通过链接构造函数和静态方法,我找到了解决此问题的方法。 我想做的事情看起来像这样:

public class Foo extends Baz {private final Bar myBar;public Foo(String arg1, String arg2) {// ...// ... Some other stuff needed to construct a 'Bar'...// ...final Bar b = new Bar(arg1, arg2);super(b.baz()):myBar = b;} } 

因此,基本上是基于构造函数参数构造一个对象,将该对象存储在一个成员中,并将该对象上的方法结果传递给super的构造函数。 使成员成为最终成员也很重要,因为该类的性质是不变的。 请注意,碰巧碰巧,构造Bar实际上需要几个中间对象,因此在我的实际用例中,它不能简化为单一形式。

我最终使它像这样工作:

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;} } 

合法代码,它完成了在调用超级构造函数之前执行多个语句的任务。

  相关解决方案