为什么在构造函数中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个回答

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()会打开一个可怕的构造函数潘多拉盒子。
另一方面,我理解经常需要在调用super()this()之前包含一些静态部分的需求。这可能是任何不依赖于this引用的代码(实际上,在构造函数的开头就已经存在,但在super()this()返回之前无法有序使用),并且需要进行这样的调用。此外,与任何方法一样,有可能在调用super()this()之前创建一些局部变量,并在其后需要使用这些变量。
在这种情况下,您有以下机会:
  1. 使用此答案中提出的模式,它允许规避限制。
  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()之外的任何东西之前返回错误要好。


虽然有点晚了,但你也可以使用工厂模式。将构造函数设为私有,将与构造函数相关的静态方法创建。我们称这个类为Foo,它有两个构造函数:Foo()和Foo(int i),以及用于构造它的静态方法createFoo()和createFoo(int i)。然后用Foo.createFoo()替换this()。因此,你可以在createFoo(int i)中做一些事情,最后执行Foo.createFoo。或者任何其他顺序。这有点像工厂设计模式,但不完全相同。 - George Xavier

3
我猜他们这样做是为了方便处理Java代码的工具和阅读Java代码的人。如果允许super()this()调用移动,就要检查更多的变化。例如,如果将super()this()调用移入条件语句if()中,它可能需要聪明地将隐式super()插入到else中。它可能需要知道如何报告错误,如果您调用super()两次,或者同时使用super()this()。它可能需要禁止在调用super()this()之前在接收器上进行方法调用,并且弄清楚什么时候调用会变得复杂。让每个人都做这份额外的工作可能看起来成本大于收益。

编写一个合理的语法规则对于这个特性本身来说就很难 - 这样的语法规则将匹配一个语句树,其中最多只有一个叶节点是显式的超级构造函数调用。我可以想到一种编写它的方法,但我的方法会相当疯狂。 - Hakanai

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

这将输出:

在父类中
在初始化器中
在子类中


3
但是这并没有比在 "super()" 后添加 System.out.println("In initializer") 作为第一行更有用,对吗?有用的是一种在 父类 构造之前执行代码的方式。 - Svend Hansen
确实。如果你想要添加一些东西,你需要在某个地方保存计算的状态。即使编译器允许你这样做,那么临时存储在哪里呢?为初始化再分配一个字段吗?但这是浪费内存。 - Val
2
这是不正确的。实例初始化程序是在父类构造函数调用返回后插入的。 - Antimony

2

Java语言架构师Brian Goetz在Amber专家组邮件列表中对此发表了评论:

历史上,this()或super()必须在构造函数中首先调用。这个限制从来不受欢迎,被认为是武断的。有许多微妙的原因,包括invokespecial的验证,导致了这种限制。多年来,我们已经在VM层面解决了这些问题,以至于考虑取消这个限制变得实际可行,不仅适用于记录,而且适用于所有构造函数。


引用应该注明来源。 - M. Justin

1
我知道我来晚了一点,但我已经使用过这个技巧几次了(而且我知道它有点不寻常):
我创建了一个通用接口 InfoRunnable,其中包含一个方法:
public T run(Object... args);

如果我需要在将其传递给构造函数之前执行某些操作,我只需这样做:

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

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

编译器只有在基类中有默认构造函数时才会隐式添加super()。它无法调用参数值。 - The incredible Jan

1

这是因为您的构造函数依赖于其他构造函数。为了使您的构造函数正确工作,必须先检查依赖的其他构造函数是否正确工作,这些构造函数由您的构造函数中的this()或super()调用。如果由this()或super()调用的其他构造函数存在问题,则执行其他语句没有意义,因为如果调用的构造函数失败,所有语句都会失败。


1

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

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

0

在你构建子对象之前,必须先创建父对象。 就像你编写以下类时所知道的:

public MyClass {
        public MyClass(String someArg) {
                System.out.println(someArg);
        }
}

它转到下一个(extend和super只是隐藏的):

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

0
在子类构造函数中添加super()的主要目的是编译器的主要工作是直接或间接地将所有类与Object类连接起来,因此编译器会检查我们是否提供了super(parameterized),如果提供了,则编译器不承担任何责任,以便将所有实例成员从Object初始化到子类。

“责任”是什么?好像你能起诉编译器一样... :) - The incredible Jan

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