如果在构造函数中使用 super 调用重写方法会发生什么

What happens if you call an overridden method using super in a constructor

有两个classes Super1Sub1

Super1.class

public class Super1 {
    Super1 (){
        this.printThree();
    }

    public void printThree(){
        System.out.println("Print Three");
    }    
}

Sub1.class

public class Sub1 extends Super1 {
    Sub1 (){
        super.printThree();
    }

    int three=(int) Math.PI;

    public void printThree(){
        System.out.println(three);
    }

    public static void main(String ...a){
         new Sub1().printThree();
    }
}

当我调用 class Sub1 的方法 printThree 时,我希望输出为:

Print Three
3

因为 Sub1 构造函数调用 super.printThree();.

但是我居然得到了

0
Print Three
3

我知道 0 是 int 的默认值,但它是如何发生的?

您看到了三件事的影响:

  1. 默认超级构造函数调用,并且

  2. 相对于超级调用的实例初始值设定项,并且

  3. 覆盖方法的工作原理

你的 Sub1 构造函数是 真的 这个:

Sub1(){
    super();               // <== Default super() call, inserted by the compiler
    three=(int) Math.PI;   // <== Instance initializers are really inserted
                           // into constructors by the compiler
    super.printThree();
}

(令人惊讶,我知道,但这是真的。使用 javap -c YourClass 来查看。:-) )

原因看起来是超类必须有机会初始化对象的一部分然后子类可以初始化对象的 its 部分。所以你得到了这种交织的效果。

考虑到这就是 Sub1 真正 的样子,让我们来看看它:

  1. JVM 创建实例并将所有实例字段设置为其默认值(所有位关闭)。所以此时, three 字段存在,并且具有值 0.

  2. JVM调用Sub1.

  3. Sub1 立即调用 super() (Super1),这...

    1. ...调用 printThree。由于 printThree 被覆盖,即使在 Super1 的代码中调用它,它也是 overridden 方法(Sub1 中的方法)被调用。这是 Java 如何实现多态性的一部分。由于 three 的实例初始化程序尚未 运行,因此 three 包含 0,这就是输出的内容。

    2. Super1returns.

  4. 回到 Sub1,编译器插入(重定位,真的)运行s 并给出 threethree 的实例初始化代码一个新值。

  5. Sub1 呼叫 printThree。由于 three 的实例初始化代码现在有 运行,printThree 打印 3.

关于将此实例初始化代码移入构造函数,您可能想知道:如果我有多个构造函数怎么办?代码移入哪个?答案是编译器 将代码 复制到每个构造函数中。 (你也可以在 javap -c 中看到。)(如果你有一个非常复杂的实例初始化器,如果编译器有效地将它变成一个方法,我不会感到惊讶,但我没有看。)

如果你做了一些非常顽皮的事情并在你的实例初始化期间调用一个方法,那就更清楚了:(live copy)

class Super
{
    public static void main (String[] args) {
        new Sub();
    }

    Super() {
        System.out.println("Super constructor");
        this.printThree();
    }

    protected void printThree() {
        System.out.println("Super's printThree");
    }
}
class Sub extends Super
{
    int three = this.initThree();

    Sub() {
        this.printThree();
    }

    private int initThree() {
        System.out.println("Sub's initThree");
        return 3;
    }

    protected void printThree() {
        System.out.println("Sub's printThree: " + this.three);
    }
}

输出:

Super constructor
Sub's printThree: 0
Sub's initThree
Sub's printThree: 3

请注意 "Sub's initThree" 的顺序。

创建实例时,会调用 Sub1 构造函数。

any 构造函数中的第一条指令是调用超类构造函数。如果您没有显式调用,则会隐式调用 Super1.

的无参数构造函数

无参数构造函数正在调用 this.printThree()。此方法在 Sub1 中被覆盖。现在,这部分可能会令人困惑,但即使代码在超类中,this.method() 仍然指的是覆盖方法。

因此它在 Sub1 中调用 printThree(),它打印变量 three - 0.

的未初始化值

现在超类的构造函数已经完成,它完成 Sub1 构造函数,它使用 super.printThree()。由于它特别说明 super,因此使用 Super1 中的方法而不是覆盖方法。这会打印 Print Three.

现在 Sub1 构造函数也完成了,main 调用了新实例的 printThree()。现在 three 已经初始化,所以你得到输出 3.

虽然之前的回答给了你明确的答案,但他们没有给你任何关于如何避免将来出现问题的指示,所以我也想补充一下我的意见。

如果你要继承,那么你应该尽可能使超级class构造函数成为"dumb"。例如

public class Super{
private int a,b;
 public Super(int a, int b) {
 this.a = a;
 this.b = b;
 }
//all the methods operating on the data provided by constructor
}

然后有这样的子构造函数

  private int c,d;
    public Sub(int a, int b) {
    super(a,b);
    c = a;
    d = b;
    }

非常好,会给你最小的 side-effects,同时保持 parent class.

的功能

但是有

public Super(){
method1();
method2();
}

然后让子做这个

public Sub(){
super.method1();
super.method2();
}

真是自找麻烦而且可能难以跟踪错误。 object 在初始化期间做的越少越好,因为它给了孩子的灵活性。 管理继承就像是愚蠢的经理与聪明的经理。愚蠢的经理称蒂姆和特雷西为员工,因为他们都是员工,而他们作为会计和人力资源经理的工作只是标签。聪明的经理知道 Tim 和 Tracy 是会计兼经理,并不关心他们基本上只是雇员。