繁体   English   中英

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

[英]How does the “this” keyword in Java inheritance work?

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

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”指向父构造函数中的Son,但“this.x”指向Father中的“x”字段。 “this”关键字如何运作?

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

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

这里有两件事情,让我们来看看它们:

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

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并添加此构造函数,该怎么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" 要摆脱这种情况,可以做一些事情。

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

有些人认为这是构造者本身概念的缺陷; 有些语言在这个意义上甚至不使用构造函数。 您可以使用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()是多态的,它甚至可以在存在子类时初始化对象,这与构造函数不同。

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

如其他所述,您不能覆盖字段,您只能隐藏它们。 JLS 8.3。 现场声明

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

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

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

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

类继承自其直接超类和直接超接口超类和超接口的所有非私有字段,这些字段既可以访问类中的代码,也不会被类中的声明隐藏。

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

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

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

在对立面中, toString()方法被覆盖:将始终被调用的实现是实例化的类之一(除非它不覆盖它),即在你的情况下Son ,无论变量的类型是什么( ObjectFather 。 ..)。

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

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

从父亲来看,它无法访问Son的x变量。

多态方法调用仅适用于实例方法。 您总是可以使用更通用的引用变量类型(超类或接口)引用对象,但在运行时,基于实际对象(而不是引用类型)动态选择的唯一事物是实例方法NOT STATIC METHODS 。 不是变量 仅根据实际对象的类型动态调用重写的实例方法。

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

解释你的代码:

System.out.println(this);

Object类型是Son因此将调用toString()方法的重写的Son版本。

System.out.println(this.x);

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

请参阅: java中的多态性

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

class Father {
    public String x = "Father";

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

当您创建Son的实例时,这将创建名为x 2个不同变量。 一个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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM