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

723

为什么在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
    }
}

太阳编译器提示: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

为什么编译器有这些限制?您能否给出一个代码示例,如果编译器没有此限制,会发生什么糟糕的事情?


9
一个好问题。我在http://valjok.blogspot.com/2012/09/super-constructor-must-be-first.html和programmers.exchange上开始了类似的讨论,展示了有些情况下必须先初始化子字段而不是super()。因此,这种特性增加了做事情的复杂性,而是否对“代码安全性”产生积极影响的问题并不清楚是否能够抵消负面影响。是的,super始终排第一也会有负面后果。令人惊讶的是没有人提到过这一点。我认为这是一个概念性的问题,应该在程序员交流平台上询问。 - Val
60
最糟糕的是,这完全是Java语言的限制。在字节码层面上并没有这样的限制。 - Antimony
2
在字节码级别上实施这样的限制是不可能的 - 即使将所有逻辑都塞进一个表达式中的示例也将违反这样的限制。 - celticminstrel
可能是[调用super()必须是构造函数主体中的第一条语句]的重复问题(https://dev59.com/rYPba4cB1Zd3GeqPzO8Q)。 - Chandrahas Aroori
22个回答

217

在调用子类构造函数之前,需要先调用父类构造函数。这样可以确保在你的构造函数中调用父类的任何方法时,父类已经被正确设置。

你试图做的事情,即将参数传递给超类构造函数是完全合法的,你只需要像你正在做的那样内联构造这些参数,或者将它们传递给你的构造函数,然后再传递给 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);
        }
}
在父类有默认构造函数的情况下,编译器会自动为您插入对super的调用。由于Java中的每个类都继承自Object,因此必须以某种方式调用对象构造函数,并且必须首先执行它。编译器通过自动插入super()来实现这一点。强制要求super出现在第一位,强制执行构造函数体按正确顺序执行:Object -> Parent -> Child -> ChildOfChild -> SoOnSoForth。

239
我想我不同意,有两个原因...(1)仅检查super是否为第一条语句是不足以防止该问题的发生。例如,您可以在构造函数中放置“super(someMethodInSuper());”。即使super是第一条语句,它也会尝试在超类构造之前访问超类中的方法。(2)编译器似乎实现了一个不同的检查,这本身足以防止这个问题。该消息为“在调用超类型构造函数之前无法引用xxx”。因此,检查super是否为第一条语句并非必要。 - Joe Daley
7
@Joe 你说得对,将super()放在第一条语句并不能防止在调用它之前调用父类的方法。正如你提到的那样,那是一个单独的检查。但是,这确实强制执行了构造函数体的执行顺序,你同意吗?我相信这就是将调用super()的语句放在第一条的原因。 - anio
10
考虑到编译器知道何时访问父类的方法/字段,因此我不明白为什么不能允许类似于“Constructor(int x) { this.field1 = x; super(); }”这样的写法。当然,在理想的情况下,您不应该需要这样做,因为您可以控制代码,但并非总是如此。我查找这个问题的原因是因为我很烦恼无法使用它来解决第三方代码中的缺陷。 - Vala
40
同意@JoeDaley的观点,我认为C#没有这个限制足以表明这个问题可以用更加巧妙的方式来解决。 - Tom Lianza
7
很多时候,当你觉得需要在调用“super”之前进行逻辑处理时,使用组合而不是继承可能更好。 - Aleksandr Dubinsky
显示剩余6条评论

113

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

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的构造函数中。由于该类是不可变的,因此使成员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;
  }
}

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


这种技术可以扩展。如果超类需要许多参数或者您需要同时设置其他字段,可以创建一个静态内部类来保存所有变量,并使用它将数据从静态方法传递到单参数构造函数。 - Aleksandr Dubinsky
25
提醒一下,很多时候,在调用“super”之前需要进行逻辑处理的情况下,采用组合而不是继承更为可行。 - Aleksandr Dubinsky
我花了一点时间才理解你的概念。基本上,你创建了一个静态方法并将其放入构造函数中。 - murt
1
@AleksandrDubinsky,您能否详细说明(提供示例代码),展示如何使用静态内部类同时设置多个超级参数?也许这在另一篇您可以链接的帖子中有更详细的讨论? - Gili
2
+1,这解决了Java限制所创建的问题。但它并没有回答OP的问题,即为什么Java编译器会有这些限制? - 1Emax
@AleksandrDubinsky 我同意使用组合的方式,但是如果我想要为一些继承类覆盖几个方法,组合就不起作用了,你只需要定义一个专门的方法来实现组合。 - undefined

60
因为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月:在消息记录:构建和验证中,Oracle建议取消此限制(但与C#不同,this将在构造函数链接之前绝对未分配(DU)。)历史上,this()或super()必须是构造函数中的第一项。这种限制从来没有受到欢迎,被视为武断。有许多微妙的原因,包括验证invokespecial,导致了这种限制。多年来,我们已经在VM级别解决了这些问题,以至于可以考虑取消这种限制,不仅适用于记录,而且适用于所有构造函数。

1
只是澄清一下:你在示例中使用的 fn() 应该是一个静态方法,对吗? - Jason S
10
+1 只是 JLS 的限制。在字节码级别上,在调用构造函数之前,您可以做其他事情。 - Antimony
3
等一下,这怎么会使语言规范变得复杂?一旦规范说明第一条语句可以是构造函数,所有其他语句就不能是构造函数。当你取消这个限制后,规范将变成“你只能在里面写语句”。这样怎么会更加复杂呢? - Uko
2
@Uko,当你将其与相关的JVM规范部分进行比较时,你会得到答案。正如Antimony所说,这个限制在字节码级别上不存在,但是,在调用超类构造函数之前不使用正在构建的对象的要求仍然存在。因此,正确代码的定义以及如何验证其正确性填满了整个页面。在JLS中提供相同的自由需要类似的复杂性,因为JLS不能允许在字节码级别上非法的事情发生。 - Holger
3
我一直觉得当人们问“为什么X是这样?”时,回答“因为它被规定成那样”有点不够令人满意。通常情况下,当人们询问X为什么是这样时,他们实际上是在问是如何做出让X变成现在这个样子的决定的 - chharvey
显示剩余5条评论

17

之所以采用这种继承哲学,是因为根据Java语言规范,构造函数体如下所定义:

ConstructorBody: { ExplicitConstructorInvocationopt    BlockStatementsopt }

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

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

如果构造函数体不以显式构造函数调用开头并且正在声明的构造函数不是原始类Object的一部分,则构造函数体会隐式地以超类构造函数调用“super();”开头,调用其直接超类的不带参数的构造函数。以此类推……一整条链的构造函数被调用,一直回到Object的构造函数;“Java平台上的所有类都是Object的后代”。这被称为“构造函数链”。

这是为什么呢?
Java以这种方式定义ConstructorBody的原因是,他们需要保持对象的层次结构。记住继承的定义:它是扩展一个类。说到这里,你不能扩展不存在的东西。首先需要创建基类(超类),然后才能派生它(子类)。这就是为什么它们被称为父类和子类;没有父亲就没有孩子。

在技术层面上,子类会继承其父类的所有成员(字段、方法、嵌套类)。由于构造函数不是成员(它们不属于对象,而是负责创建对象),因此子类不会继承构造函数,但可以调用它们。在对象创建时,只有一个构造函数被执行。那么,在创建子类对象时,如何保证创建父类?这就涉及到“构造函数链”的概念;我们有能力从当前构造函数中调用其他构造函数(即super)。Java要求这个调用必须是子类构造函数的第一行,以维护层次结构并保证其正确性。如果您没有明确地首先创建父对象(例如如果您忘记了它),它们将隐式地为您创建。
编译时会进行检查。但我不确定在运行时会发生什么样的运行时错误,如果Java在子类构造函数的中间体内显式尝试执行基本构造函数时没有抛出编译错误...

2
我知道构造函数不会被处理为函数调用,但我认为将每个超类构造函数调用解释为this = [new object],并要求在使用this之前和构造函数返回之前定义this,在语义上足以实现所述目标。无法将父构造函数调用包装在try-catch-rethrowtry/finally块中,这使得子类构造函数无法承诺不抛出可能由超类构造函数引起的异常,即使子类能够保证... - supercat
2
这样做可以避免出现异常。它也极大地增加了安全地连接需要获取资源并将其传递给父构造函数的构造函数的难度(子构造函数需要由创建资源容器的工厂方法调用,在 try 块中调用构造函数,并在构造函数失败时丢弃容器中的任何资源)。 - supercat
2
从技术上讲,它不是第一行,而是构造函数中的第一条可执行语句。在显式构造函数调用之前添加注释是完全合法的。 - ADTC

13
我很确定(熟悉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
    }        
}

在那里,Bad1Bad2应该扩展Thing吗? - Michael Myers
10
我不同意Bad2,因为xThing中被声明并且不能在其他地方设置。至于Bad1,你肯定是对的,但当超类构造函数调用子类中重写的方法并访问子类的(尚未初始化)变量时可能会发生类似的事情。因此,这个限制有助于防止问题的某一部分……但我认为这不值得。 - maaartinus
@maaartinus 不同之处在于,超类构造函数的作者有责任调用可重写方法。因此,可以设计超类以始终具有一致的状态,如果允许子类在超类构造函数被调用之前使用对象,则这是不可能的。 - Holger

9

您问为什么,其他答案中,我认为没有真正说明为什么可以调用超类的构造函数,但只有在它是第一行时才能这样做。原因是您实际上并没有“调用”构造函数。在C++中,等效的语法是

MySubClass: MyClass {

public:

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

};

当您看到初始化程序在大括号之前单独出现时,您就知道它是特殊的。它在任何其他构造函数运行之前甚至在任何成员变量被初始化之前运行。对于Java来说也不太不同。有一种方法可以让一些代码(其他构造函数)在构造函数真正开始之前运行,在子类的任何成员初始化之前运行。这种方法是将“调用”(例如super)放在第一行。(某种程度上,那个superthis就像在第一个大括号之前,即使你在之后输入它,因为它将在你完全构建所有内容之前执行。)任何其他在大括号之后的代码(例如int c = a + b;)会使编译器说“哦,好的,没有其他构造函数,我们可以初始化所有内容。”然后它就会进行初始化操作,包括您的超类和成员等等,然后开始执行大括号之后的代码。
如果几行后它遇到一些代码,说“哦,是的,当您构造此对象时,请将这些参数传递给基类的构造函数”,那么为时已晚,这是没有意义的。所以你会得到一个编译器错误。

2
  1. 如果Java设计者想要超级构造函数隐式,他们可以直接这样做,更重要的是,这并不能解释为什么隐式超级构造函数非常有用。
  2. 在我看来,你的评论说它没有任何意义,这本身就没有任何意义。我记得我需要那个。你能证明我做了一些毫无意义的事情吗?
- Val
2
想象一下,你需要进入一个房间。门被锁住了,所以你砸了窗户,伸手进去让自己进去。在里面,距离房间中心还有一半的路程,你发现了一张纸条,上面有一把钥匙可以供你使用。但是你已经进来了。同样地,如果编译器正在执行构造函数的一半,并且它遇到了“在运行构造函数之前要对这些参数进行处理”的指令,那么它应该怎么做呢? - Kate Gregory
2
如果在现实生活中这是愚蠢的事情,那么它就是一个错误的类比。如果我有决定权,该走哪条路,那我就不会在半路上了。这个规则要求超级调用必须是构造函数中的第一个,这迫使我们打破窗户(请参见大量的问题和答案中的解决方法),而不是使用门。因此,当试图为这个规则辩论时,你把一切都搞反了。因此这个规则肯定是错的。 - Val
6
这并不反映Java代码实际编译的方式、其限制以及设计Java的真正原因。 - Antimony

6
所以,它并不会阻止你在调用super之前执行逻辑。它只是阻止你执行无法放入单个表达式的逻辑。
实际上,你可以使用多个表达式来执行逻辑,只需要将代码包装在静态函数中,并在super语句中调用它。
使用你的例子:
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;
    }
}

1
只有当超类构造函数期望一个单一的非空参数时,这才有效。 - KrishPrabakar
语言设计者本可以选择强制在调用 super()/this() 之前不允许调用实例方法或继承方法,而不是将其作为第一条语句。因此,也许 OP 想知道为什么没有这样做。 - Archit

5

我完全同意,限制太强了。使用静态帮助方法(如Tom Hawtin - tackline所建议的)或将所有“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语句,但允许在参数中使用?操作符),即使这会使语言规范更加复杂。


2
我认为这个“踩”是因为你没有回答问题,而是在评论问题。在论坛中或许还可以接受,但是SO/SE不是一个论坛 :) - ADTC
这是一个很好的例子,展示了 ?: 这个结构的类型可能会让你感到惊讶。当我阅读时,我一直在想,“这并不是不可能的——只需要使用三元运算符……噢。” - Kevin J. Chase

3
我找到了一个解决方法。
这段代码无法编译:
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);
    }
}

4
这个问题不是关于绕过的方法。实际上,在问题本身中已经可以找到一个解决方法。 - c0der
这不是一个变通方法。你仍然不能编写多行代码。 - Spenhouet

3
构造函数按照派生顺序执行是有意义的。因为超类对任何子类都一无所知,它需要执行任何初始化都是与子类分开且可能是先决条件。因此,它必须先完成自己的执行。
一个简单的演示:
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(...,...)方法。 - Mohsen Abasi

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接