首页 文章

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

提问于
浏览
526

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之前是 not stopping you from executing logic . 它只是阻止你执行不能放入单个表达式的逻辑 .

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

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

19 回答

  • 0

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

    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() 之前可能需要创建一些局部变量 .

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

    • 使用this answer中显示的模式,允许规避限制 .

    • 等待Java团队允许预先 super() 和pre- 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;
    }
    

    错误"variable x might not have been initialized",它可以在 this 变量上执行此操作,对其进行检查就像在任何其他局部变量上一样 . 唯一的区别是 this 不能通过 super()this() 调用以外的任何方式分配(和通常一样,如果构造函数中没有这样的调用,编译器在开头隐式插入 super() )并且可能不会被分配两次 . 如果有任何疑问(例如在第一个 get() 中,实际上总是分配 x ),编译器可能会返回错误 . 这比在 super()this() 之前除了注释之外的任何构造函数返回错误更好 .

  • 83

    需要在子类' 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 - > Parent - > Child - > ChildOfChild - > SoOnSoForth

  • 9

    我完全同意,限制太强了 . 使用静态辅助方法(如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),即使这会使语言规范复杂化 .

  • 2

    我相当肯定(那些熟悉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
        }        
    }
    
  • 1
    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将无法获得正确的值 .

  • 5

    那是因为你的构造函数依赖于其他构造函数 . 对于您的构造函数正常工作,其他构造函数必须正常工作,这是依赖的 . 这就是为什么必须先检查依赖构造函数,然后在构造函数中调用this()或super() . 如果由this()或super()调用的其他构造函数有问题,那么什么点执行其他语句,因为如果调用构造函数失败,则所有语句都会失败 .

  • 0

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

    ConstructorBody:

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

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

    如果构造函数体不以显式构造函数调用开始,并且声明的构造函数不是原始类Object的一部分,那么构造函数体隐式地以超类构造函数调用"super();"开始,调用其直接超类的构造函数没有争论 . 等等......将会有一整个构造函数链一直被称为Object的构造函数; "All Classes in the Java platform are Descendants of Object" . 这件事叫做“ Constructor Chaining ” .

    Now why is this?
    Java以这种方式定义ConstructorBody的原因是它们需要 maintain the hierarchy 的对象 . 记住继承的定义;它存在于's extending a class. With that being said, you cannot extend something that doesn' . 需要首先创建基类(超类),然后可以派生它(子类) . 那个's why they called them Parent and Child classes; you can'有一个没有父母的孩子 .

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

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

  • 2

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

  • 13

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

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

    这将输出:

    在父母中初始化者在孩子

  • 36

    因此,它不会阻止您在调用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;
        }
    }
    
  • 162

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

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

    public T run(Object... args);
    

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

    super(new InfoRunnable<ThingToPass>() {
        public ThingToPass run(Object... args) {
            /* do your things here */
        }
    }.run(/* args here */));
    
  • 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();
        }
    }
    

    该程序的输出是:

    Inside A's constructor
    Inside B's constructor
    Inside C's constructor
    
  • 10

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

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

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

  • 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 . 我们无法在 Object 之前创建 MyClass . 简单的规则是必须在子构造函数之前调用父的构造函数 . 但我们知道类可以有更多的构造函数 . Java允许我们选择一个将被调用的构造函数(它将是 super()super(yourArgs...) ) . 因此,当您编写 super(yourArgs...) 时,您将重新定义将被调用以创建父对象的构造函数 . 您无法在 super() 之前执行其他方法,因为该对象尚不存在(但在 super() 之后将创建一个对象并且您将能够执行任何您想要的操作) .

    那么为什么我们不能在任何方法之后执行 this() ?如您所知 this() 是当前类的构造函数 . 我们的类中也可以有不同数量的构造函数,并将它们称为 this()this(yourArgs...) . 正如我所说,每个构造函数都有隐藏方法 super() . 当我们编写自定义 super(yourArgs...) 时,我们使用 super(yourArgs...) 删除 super() . 另外,当我们定义 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);
            }
    }
    
  • 4

    Tldr:

    其他答案已经解决了这个问题的问题 . 我将围绕此限制提供 hack

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

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

    唯一的限制是 parent class must have a constructor which takes in at least one argument 以便我们可以将我们的声明作为表达式潜入 .

    这是一个更详细的例子:

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

    Reworked into:

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

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

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

    你问为什么,以及其他答案,imo,你好't really say why it'可以把你的超级's constructor, but only if it'称为第一行 . 原因是你并没有真正调用构造函数 . 在C中,等效语法是

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

    当您在开放式大括号之前看到自己的初始化子句时,您知道它与Java没有什么不同 . 在初始化子类的任何成员之前,有一种方法可以在构造函数真正启动之前运行一些代码(其他构造函数) . 这种方式是将"call"(例如 super )放在第一行 . (在某种程度上, superthis 在第一个打开大括号之前就已经存在了,即使你之后输入它,因为它会在你完成所有内容之前执行 . )开放大括号之后的任何其他代码(比如 int c = a + b; )让编译器说"oh, ok, no other constructors, we can initialize everything then."所以它运行并初始化你的超类和你的成员以及诸如此类的东西,然后在开括号后开始执行代码 .

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

  • 1

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

    Edit March 2018: 在消息Records: construction and validation中Oracle建议删除此限制(但与C#不同, this 在构造函数链接之前肯定是未分配的(DU)) .

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

  • 0

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

    如果允许 super()this() 呼叫四处移动,则需要检查更多变体 . 例如,如果将 super()this() 调用移动到条件 if() 中,则可能必须足够聪明才能将隐式 super() 插入 else . 如果您两次调用 super() ,或者同时使用 super()this() ,则可能需要知道如何报告错误 . 它可能需要禁止接收器上的方法调用,直到 super()this() 被调用并找出何时变得复杂 .

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

相关问题