Java继承中的“this”关键字是如何工作的?

2022-09-01 14:02:37

在下面的代码片段中,结果确实令人困惑。

public class TestInheritance {
    public static void main(String[] args) {
        new Son();
        /*
        Father father = new Son();
        System.out.println(father); //[1]I know the result is "I'm Son" here
        */
    }
}

class Father {
    public String x = "Father";

    @Override
    public String toString() {
       return "I'm Father";
    }

    public Father() {
        System.out.println(this);//[2]It is called in Father constructor
        System.out.println(this.x);
    }
}

class Son extends Father {
    public String x = "Son";

    @Override
    public String toString() {
        return "I'm Son";
    }
}

结果是

I'm Son
Father

为什么“this”在 Father 构造函数中指向 Son,而 “this.x” 指向 Father 中的 “x” 字段。“this”关键字是如何工作的?

我知道多态概念,但[1]和[2]之间不会有区别吗?当新的Son()被触发时,内存中发生了什么?


答案 1

默认情况下,所有成员函数在 Java 中都是多态的。这意味着当您调用 this.toString() 时,Java 使用动态绑定来解析调用,调用子版本。访问成员 x 时,将访问当前作用域的成员(父),因为成员不是多态的。


答案 2

这里发生了两件事,让我们来看看它们:

首先,您要创建两个不同的字段。看一下字节码的(非常孤立的)块,你会看到这个:

class Father {
  public java.lang.String x;

  // Method descriptor #17 ()V
  // Stack: 2, Locals: 1
  public Father();
        ...
    10  getstatic java.lang.System.out : java.io.PrintStream [23]
    13  aload_0 [this]
    14  invokevirtual java.io.PrintStream.println(java.lang.Object) : void [29]
    17  getstatic java.lang.System.out : java.io.PrintStream [23]
    20  aload_0 [this]
    21  getfield Father.x : java.lang.String [21]
    24  invokevirtual java.io.PrintStream.println(java.lang.String) : void [35]
    27  return
}

class Son extends Father {

  // Field descriptor #6 Ljava/lang/String;
  public java.lang.String x;
}

重要的是第13,20和21行;其他的表示本身,或隐含的. 加载引用,从对象中检索字段值,在本例中,从 中检索。您在此处看到的是字段名称是限定的:。在 中的一行中,您可以看到有一个单独的字段。但从未使用过;唯一的就是。System.out.println();return;aload_0thisgetfieldthisFather.xSonSon.xFather.x

现在,如果我们删除并添加此构造函数,该怎么办:Son.x

public Son() {
    x = "Son";
}

首先看一下字节码:

class Son extends Father {
  // Field descriptor #6 Ljava/lang/String;
  public java.lang.String x;

  // Method descriptor #8 ()V
  // Stack: 2, Locals: 1
  Son();
     0  aload_0 [this]
     1  invokespecial Father() [10]
     4  aload_0 [this]
     5  ldc <String "Son"> [12]
     7  putfield Son.x : java.lang.String [13]
    10  return
}

第 4、5 和 7 行看起来不错:并且已加载,并且字段设置为 。为什么?因为 JVM 可以找到继承的字段。但重要的是要注意,即使该字段被引用为,JVM找到的字段实际上是。this"Son"putfieldSon.xSon.xFather.x

那么它是否给出了正确的输出呢?很遗憾,没有:

I'm Son
Father

原因是语句的顺序。字节码中的第 0 行和第 1 行是隐式调用,因此语句的顺序如下所示:super();

System.out.println(this);
System.out.println(this.x);
x = "Son";

当然,它将打印。为了摆脱这种情况,可以做一些事情。"Father"

可能最干净的是:不要在构造函数中打印!只要构造函数尚未完成,对象就不会完全初始化。您正在假设,由于 s 是构造函数中的最后一个语句,因此您的对象是完整的。正如您所体验的那样,当您有子类时,情况并非如此,因为超类构造函数将始终在子类有机会初始化对象之前完成。println

有些人认为这是构造函数本身概念的缺陷;有些语言甚至不在这个意义上使用构造函数。您可以改用 init() 方法。在普通方法中,您具有多态性的优势,因此您可以调用引用,并被调用;然而, 总是创建一个对象。(当然,在Java中,你仍然需要在某个时候调用正确的构造函数)。init()FatherSon.init()new Father()Father

但我认为你需要的是这样的东西:

class Father {
    public String x;

    public Father() {
        init();
        System.out.println(this);//[2]It is called in Father constructor
        System.out.println(this.x);
    }

    protected void init() {
        x = "Father";
    }

    @Override
    public String toString() {
        return "I'm Father";
    }
}

class Son extends Father {
    @Override
    protected void init() {
        //you could do super.init(); here in cases where it's possibly not redundant
        x = "Son";
    }

    @Override
    public String toString() {
        return "I'm Son";
    }
}

我没有它的名字,但试试吧。它将打印

I'm Son
Son

这到底是怎么回事呢?最上面的构造函数(的 构造函数)调用一个方法,该方法在子类中被重写。由于所有构造函数都首先调用,因此它们有效地执行超类到子类。因此,如果最顶层构造函数的第一次调用是,则所有 init 都发生在任何构造函数代码之前。如果 init 方法完全初始化对象,则所有构造函数都可以使用初始化的对象。由于它是多态的,它甚至可以在有子类时初始化对象,这与构造函数不同。Fatherinit()super();init();init()

请注意,这是受保护的:子类将能够调用和覆盖它,但其他包中的类将无法调用它。这是一个轻微的改进,也应该考虑。init()publicx