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

Joe*_*ley 572 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
    }
}
Run Code Online (Sandbox Code Playgroud)

Sun编译器说"调用super必须是构造函数中的第一个语句".Eclipse编译器说"构造函数调用必须是构造函数中的第一个语句".

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

public class MySubClass extends MyClass {
    public MySubClass(int a, int b) {
        super(a + b);  // OK
    }
}
Run Code Online (Sandbox Code Playgroud)

这是另一个例子:

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
    }
}
Run Code Online (Sandbox Code Playgroud)

因此,它不会阻止您在调用super之前执行逻辑.它只是阻止你执行不能适合单个表达式的逻辑.

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

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

ani*_*nio 179

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

你想要做的是,将args传递给超级构造函数是完全合法的,你只需要像你一样构造那些args内联,或者将它们传递给你的构造函数然后将它们传递给super:

public MySubClassB extends MyClass {
        public MySubClassB(Object[] myArray) {
                super(myArray);
        }
}
Run Code Online (Sandbox Code Playgroud)

如果编译器没有强制执行此操作,您可以这样做:

public MySubClassB extends MyClass {
        public MySubClassB(Object[] myArray) {
                someMethodOnSuper(); //ERROR super not yet constructed
                super(myArray);
        }
}
Run Code Online (Sandbox Code Playgroud)

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

  • 我认为我不同意,原因有两个:(1)检查超级是第一个声明是不足以防止这个问题.例如,你可以把"super(someMethodInSuper());" 在你的构造函数中.这会尝试在构造超类之前访问它,即使super是第一个语句.(2)编译器似乎实现了一个不同的检查,它本身就足以防止这个问题.消息是"在调用超类型构造函数之前无法引用xxx".因此,检查super是第一个语句是没有必要的. (186认同)
  • 同意@JoeDaley我认为C#没有这个限制的事实足以表明这个问题可以用较少的火腿方式解决. (30认同)
  • 考虑到编译器知道您何时访问父方法/字段,我不明白为什么不能允许您使用`Constructor(int x){this.field1 = x; 超(); }`.当然,您不应该在理想的世界中控制代码,但情况并非总是如此.我之所以这么认为是因为我很生气我不能用它来解决第三方代码中的缺陷. (8认同)
  • 仅供参考,通常在调用`super`之前你需要做逻辑时,你最好使用合成而不是继承. (7认同)
  • @Joe你是对的,放置super()作为第一个语句不会阻止在调用之前调用父方法.如你所说,这是一个单独的检查.但是它是否强制执行构造函数体的执行顺序?同意?我相信这是调用super()作为第一个语句的原因. (3认同)
  • @Tom确实,但这不是C#,我们已经确定Sun在决定某些结构时有点愚蠢.:/ (3认同)
  • @TomLianza 是什么让你认为 C# 没有这个限制?基本构造函数调用始终在执行构造函数主体之前发生,并且如果您在基本调用中嵌套表达式,则只能访问静态成员。 (2认同)

pen*_*dor 96

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

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;
  }
}
Run Code Online (Sandbox Code Playgroud)

所以基本上构造一个基于构造函数参数的对象,将对象存储在一个成员中,并将该对象的方法结果传递给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;
  }
}
Run Code Online (Sandbox Code Playgroud)

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

  • 仅供参考,通常在调用`super`之前你需要做逻辑时,你最好使用合成而不是继承. (14认同)
  • +1,这解决了 Java 限制造成的问题。但这并没有回答OP的问题,这就是为什么Java编译器有这些限制? (2认同)

Tom*_*ine 45

因为JLS这么说.是否可以以兼容的方式更改JLS以允许它?对.但是,它会使语言规范变得复杂,这已经非常复杂了.它不是一个非常有用的东西,它有很多方法(用方法的结果调用另一个构造函数this(fn())- 该方法在另一个构造函数之前调用,因此也是超级构造函数).因此,进行改变的功率重量比是不利的.

编辑20183月:在消息记录中:构造和验证 Oracle建议删除此限制(但与C#不同,在构造函数链接之前肯定MyDerived.fn会取消分配(DU)).

从历史上看,this()或super()必须是构造函数中的第一个.这种限制从未受到欢迎,并被认为是武断的.有许多微妙的原因,包括验证特殊参与,导致了这种限制.多年来,我们已经在虚拟机层面解决了这些问题,以至于考虑解除这一限制变得切实可行,不仅仅是记录,而是所有构造函数.

  • 提及这纯粹是JLS限制的+1.在字节码级别,您可以在调用构造函数之前执行其他操作. (8认同)
  • 对于“为什么 X 是这样的?”这个问题,我总是找到“因为它是这样指定的”答案。比不满意还多一点。通常,当人们问为什么 X 是现在这个样子时,他们实际上是在问“如何决定让 X 变成现在这样”。 (3认同)
  • 等等,这怎么会使语言规范复杂化?当规范说第一个语句可能是构造函数时,所有其他语句都不能是构造函数.当你删除限制时,规范就会像"你只是在里面有声明".这怎么更复杂? (2认同)
  • @Uko当您将其与相关的JVM规范部分进行比较时,您会得到答案.正如Antimony所说,这个限制在字节代码级别上不存在,但是,当然,在调用超级构造函数之前调用超级构造函数并且不使用正在构造的对象的要求仍然存在.因此,正确代码的定义以及如何验证其正确性将填满整个页面.在JLS中提供相同的自由度需要类似的复杂性,因为JLS不允许在字节代码级别上非法的东西. (2认同)

Jas*_*n S 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
    }        
}
Run Code Online (Sandbox Code Playgroud)

  • 我不同意'Bad2`,因为'x`在`Thing`中声明,并且不能在其他任何地方设置.至于'Bad1`,你肯定是正确的,但是当超级构造函数调用子类中重写的方法来访问子类的(尚未初始化的)变量时,可能会发生类似的事情.所以限制有助于防止问题的一部分......这是恕我直言,不值得. (9认同)

Ran*_*ity 12

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

ConstructorBody:{ExplicitConstructorInvocation opt    BlockStatements opt }

构造函数体的第一个语句可能是:
- 显式调用同一个类的另一个构造函数(通过使用关键字"this")或
直接超类(通过使用关键字"super")

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

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

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

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

  • 我知道构造函数不是作为函数调用处理的,但我认为将每个超级构造函数的调用解释为`this = [new object]`并要求在使用它之前定义`this`并在构造函数返回之前在语义上足以实现既定目标.无法在`try-catch-rethrow`或`try/finally`块中包装父构造函数调用使得不可能让子类构造函数承诺不抛出超类构造函数可能的东西,即使子类能够保证... (2认同)
  • ......不能发生异常.它还大大增加了安全链接构造函数的难度,这些构造函数需要获取资源并将它们传递给父构造函数(子构造函数需要通过为资源创建容器的工厂方法调用,在`try`块中调用构造函数,如果构造函数失败,则丢弃容器中的所有资源. (2认同)
  • 从技术上讲,它不是第一行*,而是构造函数中的第一个可执行语句。在显式构造函数调用之前添加注释是完全合法的。 (2认同)

Kat*_*ory 9

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

MySubClass: MyClass {

public:

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

};
Run Code Online (Sandbox Code Playgroud)

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

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

  • -1这并不反映代码是如何在Java中实际编译的,它的约束条件,或者是以Java的方式设计Java的实际原因. (6认同)
  • 1. 如果java设计者想要隐式超级构造函数,他们可以这样做,更重要的是,这并不能解释为什么隐式超级构造函数非常有用。2. IMO,你的评论没有任何意义,没有任何意义。我记得我需要那个。你能证明我做了无意义的事吗? (2认同)
  • 想象你需要进入一个房间.门是锁着的,所以你打碎了一扇窗户,进入并让自己进去.在房间的中间,你会找到一张带钥匙的便条,供你在途中使用.但你已经进去了.同样的,如果编译器在执行构造函数的过程中遇到了"在运行构造函数之前如何处理这些参数"它应该做什么? (2认同)
  • 如果这在现实中是愚蠢的事情,那么它就是一个错误的类比。如果我能够决定走哪条路,我就不会半途而废。规则是,超级调用必须是构造函数中的第一个,它促使我们打破窗户(请参阅问题和答案中的许多 Warkingaround 示例)而不是使用门。所以,当你试图论证这条规则时,你把一切都颠倒过来了。因此,这条规则一定是错误的。 (2认同)

Tip*_*-Sy 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;
    }
}
Run Code Online (Sandbox Code Playgroud)


Dav*_*Far 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
    }
}
Run Code Online (Sandbox Code Playgroud)

正如 Carson Myers 建议的那样,使用“尚未构造的对象”异常会有所帮助,但在每个对象构造期间检查此异常会减慢执行速度。我更喜欢 Java 编译器,它可以做出更好的区分(而不是无缘无故地禁止 if 语句但允许参数中的 ? 运算符),即使这会使语言规范复杂化。

  • 我认为投反对票是因为您不是在回答问题,而是在对该问题发表评论。在论坛中可以,但 SO/SE 不是一个 :) (2认同)