关于Java:为什么这个()和超()必须是构造函数中的第一个语句?

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

调用this()也有类似的规则。编译器说"调用这个必须是构造函数中的第一个语句"。

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


父类"EDOCX1"〔0〕需要在子类"EDOCX1"〔0〕之前调用。这将确保如果在构造函数中对父类调用任何方法,则父类已经正确设置。

您要做的是,将args传递给超级构造函数是完全合法的,您只需要像您所做的那样以内联方式构造这些args,或者将它们传递给构造函数,然后将它们传递给super

1
2
3
4
5
public MySubClassB extends MyClass {
        public MySubClassB(Object[] myArray) {
                super(myArray);
        }
}

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

1
2
3
4
5
6
public MySubClassB extends MyClass {
        public MySubClassB(Object[] myArray) {
                someMethodOnSuper(); //ERROR super not yet constructed
                super(myArray);
        }
}

如果一个parent类有一个默认的构造函数,那么compiler会自动为您插入对super的调用。由于Java中的每一个类都是从EDCOX1(5)中继承的,所以必须以某种方式调用对象构造函数,并且必须首先执行它。编译器自动插入super()允许这样做。强制super首先出现,强制以正确的顺序执行构造函数体,即:object->parent->child->childofchild->soonfourth


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

1
2
3
4
5
6
7
8
9
10
11
12
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的构造函数。使成员成为最终成员也是相当重要的,因为类的性质是不可变的。注意,正如它所发生的那样,构造条实际上需要几个中间对象,所以在我的实际用例中,它不能简化为一行程序。

我最终使它像这样工作:

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以允许它?是的。但是,它会使语言规范复杂化,这已经足够复杂了。这不是一件非常有用的事情,而且有很多方法可以解决这一问题(使用方法this(fn())的结果调用另一个构造函数-该方法在另一个构造函数之前调用,因此也是超级构造函数)。所以做这种改变的功率重量比是不利的。

编辑:2018年3月:在消息记录:构造和验证中,Oracle建议删除此限制(但与C不同的是,在构造链接之前,this肯定是未分配的(du)。

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来说,这并没有什么不同。有一种方法可以让一些代码(其他构造函数)在构造函数真正启动之前、子类的任何成员初始化之前运行。这样就可以把"呼叫"(如super)放在第一行。(从某种意义上说,superthis是在第一个开括号之前的一种类型,即使您在后面键入,因为它将在您到达完全构造之前执行。)在开括号之后的任何其他代码(如int c = a + b;使编译器说"噢,好的,没有其他的构造函数,我们可以初始化所有的所以它会运行并初始化您的超级类、成员以及其他内容,然后在左大括号后开始执行代码。

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


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


我完全同意,限制太强了。使用静态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代码。

如果允许super()this()调用四处移动,则需要检查更多的变化。例如,如果将super()this()调用移动到条件if()中,可能需要足够智能才能将隐式super()插入到else中。如果您两次调用super(),或者同时使用super()this(),它可能需要知道如何报告错误。在调用super()this()并确定何时变得复杂之前,可能需要禁止对接收器的方法调用。

让每个人做这些额外的工作似乎是一个比利益更大的成本。


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

构造过程中的一个异常几乎总是表示正在构造的对象无法正确初始化,现在处于错误状态,不可用,必须进行垃圾收集。但是,子类的构造函数能够忽略它的一个超类中发生的异常并返回一个部分初始化的对象。在上面的例子中,如果给new Bad()的参数是0或大于100,那么essential1essential2都没有正确初始化。

你可以说忽略例外总是个坏主意。好的,下面是另一个例子:

1
2
3
4
5
6
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()之前创建的一些局部变量可能会在调用之后被需要。

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

  • 使用这个答案中给出的模式,它允许绕过限制。
  • 等待Java团队允许预EDCOX1×3和预EDCOX1×4代码。它可以通过对构造函数中可能出现的super()this()进行限制来实现。实际上,即使是现在的编译器也能够区分好的和坏的(或潜在的坏的)情况,其程度足以安全地允许在构造函数的开头添加静态代码。事实上,假设super()this()返回this引用,反过来,您的构造函数
  • 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可能尚未初始化",它可以对this变量执行此操作,从而像对任何其他局部变量一样对其进行检查。唯一的区别是,this不能通过除super()this()调用以外的任何方式进行赋值(而且,与通常情况一样,如果在构造函数中没有此类调用,则super()由编译器在开始处隐式插入),并且不能进行两次赋值。如果有任何疑问(如在第一个get()中,其中x实际上总是被分配),编译器可能会返回一个错误。如果在super()this()之前有注释,那么这比简单地在任何构造函数上返回错误要好。


    我发现了一个麻烦。

    这不会编译:

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

    这将输出:

    In parent
    In initializer
    In child


    这是因为您的构造函数依赖于其他构造函数。对于您的构造函数来说,它是正确工作的必要条件,而其他构造函数则是正确工作的,这是相互依赖的。这就是为什么必须首先检查依赖构造函数的原因,依赖构造函数由构造函数中的this()或super()调用。如果此()或super()调用的其他构造函数有问题,那么什么时候执行其他语句,因为如果调用的构造函数失败,所有语句都将失败。


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

    我用一种方法创建了一个通用接口InfoRunnable

    1
    public T run(Object... args);

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

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

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


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

    1
    2
    3
    4
    5
    public MyClass {
            public MyClass(String someArg) {
                    System.out.println(someArg);
            }
    }

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

    1
    2
    3
    4
    5
    6
    public MyClass extends Object{
            public MyClass(String someArg) {
                    super();
                    System.out.println(someArg);
            }
    }

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

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    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);
            }
    }

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

    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方法链中有super()构造函数。不能这样创建对象:

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


    Tldr:

    其他的答案解决了这个问题的"为什么"。我将提供一个关于这个限制的黑客程序:

    基本思想是用嵌入的语句劫持super语句。这可以通过把你的陈述伪装成表达来实现。

    Tsdr:

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

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

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