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

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编译器说,call to super must be first statement in constructor。Eclipse编译器说,Constructor call must be the first statement in a constructor

但是,您可以通过稍微重新排列代码来解决这个问题:

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 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()之前是而不是阻止你执行逻辑。它只是阻止您执行无法放入单个表达式的逻辑。

调用this()也有类似的规则。编译器说,call to this must be first statement in constructor

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

314433 次浏览

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

你要做的是,将args传递给超级构造函数是完全合法的,你只需要像你正在做的那样内联构造这些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 constructed
super(myArray);
}
}

在父类有默认构造函数的情况下,编译器会自动插入对超函数的调用。由于Java中的每个类都继承自Object,因此必须以某种方式调用对象构造函数并且必须首先执行它。编译器自动插入超()允许这一点。强制超首先出现,强制构造函数主体以正确的顺序执行,即:对象->父->子->ChildOfChildren->SoOnSoForth

我相当肯定(熟悉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 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
}
}

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

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

这将输出:

在父级
在构造器
中 在孩子

因为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来处理它们。;(

顺便说一下,从Java1.4开始,包含外部this的合成字段在调用内部类超级构造函数之前被分配。这导致编译为目标早期版本的代码中出现特殊的NullPointerException事件。

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

2018年3月编辑:在消息记录:构建和验证 Oracle建议删除此限制(但与C#不同,this在构造函数链接之前将是绝对未分配(DU))。

从历史上看,this()或超()必须是构造函数中的第一个。这 限制从来都不受欢迎,而且被认为是武断的。有 一些微妙的原因,包括验证 调用特别,这促成了这种限制。多年来, 我们已经在VM级别解决了这些问题,以至于它变成了 考虑取消这一限制是可行的,不仅仅是为了记录, #36825;所有的构造器

我完全同意,限制太强了。使用静态辅助方法(正如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语句,而是允许?-运算符在参数中),即使这会使语言规范复杂化。

你问为什么,而其他答案,imo,并没有真正说明为什么调用你的上级的构造函数是可以的,但前提是它是第一行。原因是你不是真正的呼吁构造函数。在C++,等效的语法是

MySubClass: MyClass {


public:


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


};

当你在大括号前看到初始化子句时,你会知道它很特别。它在任何其他构造函数运行之前运行,事实上在任何成员变量初始化之前运行。这对Java来说没什么不同。有一种方法可以让一些代码(其他构造函数)在构造函数真正启动之前运行,在子类的任何成员初始化之前。这种方法是把“调用”(例如super)放在第一行。(在某种程度上,superthis有点像在第一个大括号之前,即使你在之后键入它,因为它将在你到达一切都完全构建的地步之前被执行。)开括号之后的任何其他代码(如int c = a + b;)都会让编译器说“哦,好吧,没有其他构造函数,我们可以初始化所有东西。”所以它运行并初始化你的超类和成员等等,然后在开括号之后开始执行代码。

如果几行之后,它遇到一些代码说“哦,是的,当你构造这个对象时,这是我希望你传递给基类构造函数的参数”,那就太晚了,而且没有任何意义。所以你会得到一个编译器错误。

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

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

所以基本上是根据构造函数参数构造一个对象,将对象存储在一个成员中,并将该对象上的方法的结果传递给超级的构造函数。将成员设为Final也相当重要,因为类的本质是它是不可变的。请注意,碰巧的是,构造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;
}
}

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

简单地说,因为这是继承哲学。根据Java语言规范,构造函数的主体是这样定义的:

构造器主体: EYZ0块语句调用

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

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

如果一个构造函数体不以显式的构造函数调用开始,并且被声明的构造函数不是原始类Object的一部分,那么构造函数体隐式地以超类构造函数调用“Super();”开始,对其直接超类的构造函数的调用不接受参数。等等…会有一整条构造函数链一直调用到Object的构造函数;“Java平台中的所有类都是Object的后代”。这个东西叫做“构造器链接”。

这是为什么?
Java以这种方式定义ConstructorBody的原因是他们需要保持等级制度的对象。记住继承的定义;它是在扩展一个类。话虽如此,你不能扩展不存在的东西。需要先创建基(超类),然后你才能派生它(子类)。这就是为什么他们称他们为父类和子类;没有父类你就不能有孩子。

在技术层面上,子类从其父类继承所有成员(字段、方法、嵌套类)。由于构造函数不是成员(它们不属于对象。它们负责创建对象),所以它们不被子类继承,但可以被调用。从在创建对象时,只执行一个构造函数开始。那么当你创建子类对象时,我们如何保证超类的创建?因此有了“构造函数链”的概念;因此我们有能力从当前构造函数中调用其他构造函数(即超级)。Java要求这次调用是子类构造函数中的第一行来维护层次结构并保证它。他们假设如果你没有先显式创建父对象(比如你忘记了),他们会隐式地为你做。

这个检查是在编译过程中完成的。但我不确定运行时会发生什么,我们会得到什么样的运行时错误,如果Java当我们明确尝试从子类主体中间的构造函数中而不是从第一行执行基构造函数时不抛出编译错误…

我的猜测是,他们这样做是为了让编写处理Java代码的工具的人更轻松,并且在某种程度上也让阅读Java代码的人更轻松。

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

让每个人都做这项额外的工作似乎成本大于收益。

因此,它不会阻止您在调用之前执行逻辑 太好了。它只是阻止你执行你无法适应的逻辑 一个简单的表达式

实际上,您可以执行具有多个扩展的逻辑,您只需将代码包装在静态函数中并在超级语句中调用它。

使用您的示例:

public class MySubClassC extends MyClass {
public MySubClassC(Object item) {
// Create a list that contains the item, and pass the list to super
super(createList(item));  // OK
}


private static List createList(item) {
List list = new ArrayList();
list.add(item);
return list;
}
}

Tldr:

其他答案已经解决了问题的“为什么”。我将围绕这个限制提供一个破解

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

Tsdr:

假设我们想在调用super()之前执行Statement1()Statement9()

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

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

我找到了一个沃特。

这不会编译:

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

这工作:

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

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

我用一个方法创建了一个通用接口InfoRunnable<T>

public T run(Object... args);

如果我需要在传递给构造函数之前做一些事情,我只需要这样做:

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

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

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()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);
}
}
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将无法获得正确的值。

构造函数按以下顺序完成它们的执行是有道理的 派生。因为超类不知道任何子类,任何 它需要执行的初始化独立于并且可能 子类执行的任何初始化的先决条件。 因此,它必须首先完成其执行。

一个简单的演示:

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

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

你能给出一个代码示例,如果编译器没有这个限制,就会发生不好的事情吗?

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()的参数为0或大于100,那么essential1essential2都不会正确初始化。

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

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

很有趣,不是吗?在这个例子中,我们创建了多少个对象?一个?两个?或者什么都没有……

允许在构造函数中间调用super()this()将打开一个由令人发指的构造函数组成的潘多拉盒子。


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

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

  1. 使用这个答案中提供的模式,它允许绕过限制。
  2. 等待Java团队允许预super()和预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不能通过super()this()调用以外的任何方式赋值(并且,像往常一样,如果构造函数没有此类调用,super()会由编译器在开始时隐式插入)并且可能不会被赋值两次。如果有任何疑问(比如在第一个get()中,x实际上总是被赋值),编译器可能会返回错误。这比简单地在任何构造函数上返回错误要好,其中除了super()this()之前的注释之外还有其他内容。

那是因为你的构造函数依赖于其他构造函数。要让你的构造函数正常工作,其他依赖的构造函数就必须正常工作。这就是为什么有必要首先检查你的构造函数中this()或超()调用的依赖构造函数。如果被this()或超()调用的其他构造函数有问题,那么执行其他语句有什么意义,因为如果被调用的构造函数失败,所有都将失败。

为什么Java这样做的问题已经得到了回答,但由于我偶然发现了这个问题,希望找到一个更好的替代单行代码,我在此分享我的解决方法:

public class SomethingComplicated extends SomethingComplicatedParent {


private interface Lambda<T> {
public T run();
}


public SomethingComplicated(Settings settings) {
super(((Lambda<Settings>) () -> {


// My modification code,
settings.setting1 = settings.setting2;
return settings;
}).run());
}
}

调用静态函数应该执行得更好,但如果我坚持在构造函数“内部”编写代码,或者我必须更改多个参数并发现定义许多静态方法不利于易读性,我会使用它。

在子类构造函数中添加超()的主要目标是编译器的主要工作是将所有类与Object类直接或间接连接起来,这就是为什么编译器检查我们是否提供了超(参数化),然后编译器不承担任何责任。 以便所有实例成员从Object初始化到子类。

这是官方的回放: 从历史上看,this()或超()必须在构造函数中的第一个。这
限制从来就不受欢迎,而且被认为是武断的。 一些微妙的原因,包括调用特殊的验证,
导致了这种限制。多年来,我们已经解决了
这些在VM级别,到
变得实用的地步 考虑解除此限制,不仅针对记录,而且针对所有
构造函数。