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

时间:2009-07-22 21:25:16

标签: java constructor

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 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()有类似的规则。编译器说“调用它必须是构造函数中的第一个语句”。

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

21 个答案:

答案 0 :(得分:173)

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

你要做的是,将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);
        }
}

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

答案 1 :(得分:89)

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

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

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

答案 2 :(得分:40)

因为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月:在消息Records: construction and validation中,Oracle建议删除此限制(但与C#不同,this 肯定是未分配的( DU)在构造函数链接之前。)

  

从历史上看,this()或super()必须是构造函数中的第一个。这个   限制从未流行,被认为是武断的。曾经有   一些微妙的原因,包括验证   invokespecial,这有助于这种限制。这些年来,   我们已经在VM级别解决了这些问题,直到它成为了这一点   实际考虑解除这个限制,而不仅仅是记录,   但对于所有建设者。

答案 3 :(得分: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
    }        
}

答案 4 :(得分:12)

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

<强> ConstructorBody:         {ExplicitConstructorInvocation opt BlockStatements opt }

构造函数体的第一个语句可能是

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

如果构造函数体不是以显式构造函数调用开始并且声明的构造函数不是原始类Object的一部分,那么构造函数体隐式地以超类构造函数调用“super();”开头,调用它的直接超类的构造函数,不带参数。等等......将会有一整个构造函数链一直被称为Object的构造函数; “Java平台中的所有类都是对象的后代”。这个东西叫做“ Constructor Chaining ”。

现在为什么会这样?
Java以这种方式定义ConstructorBody的原因是他们需要维护对象的层次结构。记住继承的定义;它正在扩展一个类。话虽如此,你不能扩展不存在的东西。需要首先创建基类(超类),然后可以派生它(子类)。这就是为什么他们称他们为父母和儿童班;你不能没有父母的孩子。

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

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

答案 5 :(得分:9)

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

MySubClass: MyClass {

public:

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

};

当您在开放式大括号之前看到自己的初始化子句时,您知道它是特殊的。它在任何其余构造函数运行之前运行,实际上在任何成员变量初始化之前运行。 Java并没有那么不同。在构造函数真正启动之前,有一种方法可以在初始化子类的任何成员之前运行一些代码(其他构造函数)。这种方式是将“调用”(例如super)放在第一行。 (在某种程度上,superthis在第一个打开大括号之前就已经存在了,即使你之后键入它,因为它将在你完成所有内容之前执行。 )在开括号之后的任何其他代码(如int c = a + b;)使编译器说“哦,好吧,没有其他构造函数,我们可以初始化所有内容。”所以它会运行并初始化你的超类和你的成员以及诸如此类的东西,然后在开放式大括号之后开始执行代码。

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

答案 6 :(得分:5)

  

所以,它并没有阻止你在调用之前执行逻辑   超。它只是阻止你执行你不能适应的逻辑   单个表达式。

实际上你可以用几次尝试来执行逻辑,你只需要将你的代码包装在一个静态函数中并在超级语句中调用它。

使用您的示例:

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

答案 7 :(得分:4)

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

答案 8 :(得分:3)

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

如果您允许super()this()来电移动,则需要检查更多变体。例如,如果您将super()this()调用移动到条件if(),则可能必须足够聪明才能将隐式super()插入else。如果您拨打super()两次,或者同时使用super()this(),可能需要知道如何报告错误。在调用super()this()之前,可能需要禁止对接收方进行方法调用,并确定何时变得复杂。

让每个人都做这项额外的工作似乎比成本更高。

答案 9 :(得分:3)

  

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

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()将打开Pandora的令人讨厌的构造函数框。


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

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

  1. 使用在this answer上显示的模式,它可以绕开限制。
  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()之前除了注释之外的构造函数上简单地返回错误要好。

答案 10 :(得分:2)

在调用它的构造函数之前,您可以使用匿名初始化程序块初始化子项中的字段。这个例子将证明:

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

这将输出:

  

在父母中   在初始化器中   在孩子中

答案 11 :(得分:2)

我找到了一个蠢货。

这不会编译:

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

答案 12 :(得分:2)

  

构造函数按顺序完成执行是有道理的   推导。因为超类不知道任何子类,任何子类   它需要执行的初始化是独立的,也可能是   子类执行的任何初始化的先决条件。   因此,它必须首先完成它的执行。

一个简单的演示:

head(tab, 2)

        Date Category 1 2  3  4   5   6 7
1 2008-11-28    Goods 1 3 28 47 132 123 1
2                Bads 0 0  1  4   9  27 2

该程序的输出是:

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

答案 13 :(得分:1)

我知道我参加聚会的时间有点晚了,但我曾经多次使用过这个技巧(我知道这有点不寻常):

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

public T run(Object... args);

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

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

答案 14 :(得分:1)

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

答案 15 :(得分:1)

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

答案 16 :(得分:0)

在子类构造函数中添加super()的主要目标是,编译器的主要工作是使所有类与Object类建立直接或间接连接,这就是为什么编译器检查是否具有如果提供了super(参数化),则编译器不承担任何责任。 以便所有实例成员都从Object初始化为子类。

答案 17 :(得分:0)

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

调用静态函数应该会更好,但是如果我坚持要在构造函数内部使用代码,或者必须更改多个参数并发现定义许多不利于可读性的静态方法,则可以使用此函数。

答案 18 :(得分:0)

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),请参阅示例,如果我们不在第一行调用C()那么z的值取决于y,那么它将是z的问题。 z无法获得正确的价值。

答案 19 :(得分:0)

在构造子对象之前,必须创建父对象。 正如你所知,当你写这样的课时:

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。我们无法在MyClass之前创建Object。 简单的规则是必须在子构造函数之前调用父的构造函数。 但是我们知道类可以有更多的构造函数。 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;
    }
}

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

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

答案 20 :(得分:0)

Tldr:

其他答案已经解决了&#34;为什么&#34;这个问题。我将围绕此限制提供 hack

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

TSDR:

在我们致电Statement1()之前,请考虑我们要Statement9()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;
    }
}

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