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

Posted

技术标签:

【中文标题】Java 继承中的“this”关键字是如何工作的?【英文标题】:How does the "this" keyword in Java inheritance work? 【发布时间】:2014-05-28 03:11:53 【问题描述】:

在下面的代码sn-p中,结果真的很混乱。

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]之间不会有区别吗?当 new Son() 被触发时,内存中发生了什么?

【问题讨论】:

您不能覆盖字段。如果你尝试,就会发生奇怪的事情。 这种情况说明了为什么像 TIOBE 这样的措施完全没有意义。这种愚蠢、简单和基本的(在互联网上被问了多次,包括 SO)在两分钟内收到 10 个赞成票,而我简单的三行答案在五分钟内收到 6 个赞成票。用这种流量来衡量一种语言的流行度是完全没有意义的,因为流量的质量表明了这一点。 我敢肯定,如果我用 C++ 而不是 Java 发布完全相同的问题,我会在几分钟内收到大量反对票和数千个重复标记。 @Nick 如果您是新手,这个问题本身就是一个完全合理的问题。我的意思是 SO 被设计成一个知识库,有很好的问题和很好的答案。因此,有些问题有很多赞成票,反映了该主题的重要性。但这是一个关于多态性的基本问题,这个问题之前已经在互联网上直接或间接地回答过很多次,尤其是 SO。 @Nick 我对这类基本问题的期望是人们提供指向其他参考资料的链接(在 SO 内部,即重复项),以免在 SO 网络上重复知识,而不是对问题进行大规模投票。正如我所说,如果我发布问题,我确信 C++ 人员会这样做。 【参考方案1】:

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

【讨论】:

嗨@Manu,我已经编辑了问题,你能再看一下吗?实际上我知道多态是如何工作的,但是我不清楚执行 new Son() 时内存中发生了什么,例如“this”指向哪个区域? Son和Father中的“this”有什么区别? 没有区别,这个是一样的。这是对您正在使用的对象的引用(指针)。当你实例化一个孩子时,首先调用基类的ctor,因此执行父构造函数的代码。由于 Java 方法是多态的,如果您调用基类中的方法在派生类中被覆盖(如示例中的字符串),则调用派生类的版本。 @Garnett 记住:这指向对象。但是当你在基础的范围内时,它不知道它的动态类型是否是派生类型而不是那个类型(在这种情况下是父亲)。所以如果你在父亲的范围内,这就像它只是一个父亲一样,所以 x 是父亲的 x @Garnett 在默认情况下使用非多态语言(如 C# 或 C++)尝试该代码:您将看到基类仅访问基类的内容,因此输出将是“I 'm Father.Father"(假设您没有使用现有的多态函数,例如 C# 中的 toString()。再写一个。 Println(this) 是 printlm(this.toString()) 的简写。而 toString() 是您在 child 上覆盖的多态函数。所以子版本被解析,因为那是对象的真实类型(即使在基类的代码中你也无法知道。这就是多态性的重点【参考方案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_0 加载 this 引用,getfield 从对象检索字段值,在这种情况下,从 this。您在这里看到的是字段名称是合格的:Father.x。在Son 的一行中,您可以看到有一个单独的字段。但是Son.x 从未使用过;只有Father.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 行看起来不错:this"Son" 已加载,字段设置为 putfield。为什么Son.x?因为JVM可以找到继承的字段。但需要注意的是,尽管该字段被引用为Son.x,但JVM找到的字段实际上是Father.x

那么它是否给出了正确的输出?不幸的是,没有:

I'm Son
Father

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

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

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

可能最干净的是:不要在构造函数中打印!只要构造函数还没有完成,对象就没有完全初始化。您正在假设,由于 printlns 是构造函数中的最后一个语句,因此您的对象是完整的。正如您所经历的,当您有子类时,情况并非如此,因为超类构造函数总是会在您的子类有机会初始化对象之前完成。

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

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

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

那么这里发生了什么?您最顶层的构造函数(Father 的构造函数)调用 init() 方法,该方法在子类中被覆盖。由于所有构造函数首先调用super();,因此它们有效地执行超类到子类。因此,如果最顶层的构造函数的第一次调用是init();,那么所有的初始化都发生在任何构造函数代码之前。如果您的 init 方法完全初始化了对象,那么所有构造函数都可以使用已初始化的对象。而且由于init()是多态的,它甚至可以在有子类的情况下初始化对象,这与构造函数不同。

请注意init() 是受保护的:子类将能够调用和覆盖它,但其他包中的类将无法调用它。这比public 略有改进,也应该考虑用于x

【讨论】:

【参考方案3】:

如前所述,您不能覆盖字段,只能隐藏它们。见JLS 8.3. Field Declarations

如果该类声明了一个具有特定名称的字段,则该字段的声明被称为隐藏了超类和该类的超接口中具有相同名称的任何和所有可访问的字段声明。

在这方面,隐藏字段不同于隐藏方法 (§8.4.8.3),因为在字段隐藏中没有区分静态字段和非静态字段,而区分静态和非静态字段方法隐藏中的静态方法。

如果隐藏字段是静态的,则可以使用限定名称 (§6.5.6.2) 或使用包含关键字 super (§15.11.2) 的字段访问表达式或转换为超类类型来访问隐藏字段。

在这方面,隐藏字段类似于隐藏方法。

一个类从它的直接超类和直接超接口继承超类和超接口的所有非私有字段,这些字段都可以被类中的代码访问,并且不会被类中的声明隐藏。

您可以使用super 关键字从Son 的范围内访问Father 的隐藏字段,但相反的情况是不可能的,因为Father 类不知道它的子类。

【讨论】:

【参考方案4】:

虽然可以覆盖方法,但可以隐藏属性。

在您的情况下,属性x 是隐藏的:在您的Son 类中,您无法访问Fatherx 值,除非您使用super 关键字。 Father 类不知道 Sonx 属性。

相反,toString() 方法被覆盖:将始终被调用的实现是实例化类之一(除非它不覆盖它),即在你的情况下 Son,无论变量的类型(Object, Father...)。

【讨论】:

【参考方案5】:

多态方法调用仅适用于实例方法。您始终可以使用更通用的引用变量类型(超类或接口)引用对象,但在运行时,唯一根据实际对象(而不是引用类型)动态选择的东西是实例方法 不是静态方法。非变量。只有被覆盖的实例方法会根据真实对象的类型动态调用。

所以变量x 没有多态行为,因为它不会在运行时被动态选择。

解释你的代码:

System.out.println(this);

Object 类型为Son,因此将调用toString() 方法的Overridden Son 版本。

System.out.println(this.x);

这里的对象类型不在图片中,this.xFather 类中,所以将打印x 变量的Father 版本。

查看更多信息:Polymorphism in java

【讨论】:

【参考方案6】:

这是一种专门用于访问私有成员的行为。所以 this.x 查看为父亲声明的变量 X,但是当您在父亲的方法中将此作为参数传递给 System.out.println 时 - 它会根据参数的类型查看要调用的方法 - 在您的案例儿子。

那么如何调用超类方法呢?使用super.toString()

从父亲它无法访问儿子的 x 变量。

【讨论】:

【参考方案7】:

这通常称为阴影。注意你的类声明:

class Father 
    public String x = "Father";

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

当您创建Son 的实例时,这会创建两个名为x不同 变量。一个x 属于Father 超类,第二个x 属于Son 子类。根据输出,我们可以看到,当在Father 范围内时,this 访问了Fatherx 实例变量。因此该行为与“this 指向的内容”无关;这是运行时搜索实例变量的结果。它只会在类层次结构中向上 搜索变量。一个类只能引用自身及其父类的变量;它不能直接访问其子类中的变量,因为它不知道任何关于其子类的信息。

要获得你想要的多态行为,你应该只在Father中声明x

class Father 
    public String x;

    public Father() 
        this.x = "Father"
    

class Son extends Father 
    public Son() 
        this.x = "Son"
    

本文讨论了您所遇到的行为:http://www.xyzws.com/Javafaq/what-is-variable-hiding-and-shadowing/15。

【讨论】:

以上是关于Java 继承中的“this”关键字是如何工作的?的主要内容,如果未能解决你的问题,请参考以下文章

java 关于继承中的super() 方法

java中的继承

Java笔记(继承)

Java笔记(继承)

Java笔记(继承)

Java语言中的----继承