简体   繁体   English

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

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

In the below code snippet, the result is really confusing. 在下面的代码片段中,结果确实令人困惑。

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

The result is 结果是

I'm Son
Father

Why is "this" pointing to Son in the Father constructor, but "this.x" is pointing to "x" field in Father. 为什么“this”指向父构造函数中的Son,但“this.x”指向Father中的“x”字段。 How is the "this" keyword working? “this”关键字如何运作?

I know about the polymorphic concept, but won't there be different between [1] and [2]? 我知道多态的概念,但[1]和[2]之间不会有什么不同吗? What's going on in memory when new Son() is triggered? 新Son()被触发时,内存中发生了什么?

All member functions are polymorphic in Java by default. 默认情况下,所有成员函数在Java中都是多态的。 That means when you call this.toString() Java uses dynamic binding to resolve the call, calling the child version. 这意味着当你调用this.toString()时,Java使用动态绑定来解析调用,调用子版本。 When you access the member x, you access the member of your current scope (the father) because members are not polymorphic. 当您访问成员x时,您访问当前作用域(父亲)的成员,因为成员不是多态的。

Two things are going on here, let's look at them: 这里有两件事情,让我们来看看它们:

First of all, you are creating two different fields. 首先,您要创建两个不同的字段。 Taking a look at a (very isolated) chunks of the bytecode, you see this: 看一下(非常孤立的)字节码块,你会看到:

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

Important are lines 13, 20 and 21; 重要的是第13,20和21行; the others represent the System.out.println(); 其他代表System.out.println(); itself, or the implicit return; 本身,或隐含的return; . aload_0 loads the this reference, getfield retrieves a field value from an object, in this case, from this . aload_0加载this参考, getfield从对象检索一个字段值,在此情况下,从this What you see here is that the field name is qualified: Father.x . 你在这里看到的是字段名称是合格的: Father.x In the one line in Son , you can see there is a separate field. Son一行中,您可以看到有一个单独的字段。 But Son.x is never used; Son.x从未使用过; only Father.x is. 只有Father.x是。

Now, what if we remove Son.x and instead add this constructor: 现在,如果我们删除Son.x并添加此构造函数,该怎么Son.x

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

First a look at the bytecode: 首先看一下字节码:

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
}

Lines 4, 5 and 7 look good: this and "Son" are loaded, and the field is set with putfield . 第4,5和7行看起来很好:加载了this"Son" ,并且使用putfield设置了字段。 Why Son.x ? 为什么选择Son.x because the JVM can find the inherited field. 因为JVM可以找到继承的字段。 But it's important to note that even though the field is referenced as Son.x , the field found by the JVM is actually Father.x . 但重要的是要注意,即使该字段被引用为Son.x ,JVM找到的字段实际上Father.x

So does it give the right output? 那么它能提供正确的输出吗? Unfortunately, no: 很不幸的是,不行:

I'm Son
Father

The reason is the order of statements. 原因是陈述的顺序。 Lines 0 and 1 in the bytecode are the implicit super(); 字节码中的第0行和第1行是隐式的super(); call, so the order of statements is like this: 调用,所以语句的顺序是这样的:

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

Of course it's gonna print "Father" . 当然它会打印"Father" To get rid of that, a few things could be done. 要摆脱这种情况,可以做一些事情。

Probably the cleanest is: don't print in the constructor! 可能最干净的是: 不要在构造函数中打印! As long as the constructor hasn't finished, the object is not fully initialized. 只要构造函数没有完成,对象就不会完全初始化。 You are working on the assumption that, since the println s are the last statements in your constructor, your object is complete. 您正在假设,由于println是构造函数中的最后一个语句,因此您的对象已完成。 As you have experienced, this is not true when you have subclasses, because the superclass constructor will always finish before your subclass has a chance to initialize the object. 正如您所经历的那样,当您拥有子类时,情况并非如此,因为超类构造函数将始终在您的子类有机会初始化对象之前完成。

Some see this as a flaw in the concept of constructors itself; 有些人认为这是构造者本身概念的缺陷; and some languages don't even use constructors in this sense. 有些语言在这个意义上甚至不使用构造函数。 You could use an init() method instead . 您可以使用init()方法 In ordinary methods, you have the advantage of polymorphism, so you can call init() on a Father reference, and Son.init() is invoked; 在普通方法中,您具有多态性的优点,因此可以在Father引用上调用init() ,并调用Son.init() ; whereas, new Father() always creates a Father object. new Father()总是创建一个Father对象。 (of course, in Java you still need to call the right constructor at some point). (当然,在Java中,你仍然需要在某个时候调用正确的构造函数)。

But I think what you need is something like this: 但我认为你需要的是这样的:

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 don't have a name for it, but try it out. 我没有它的名字,但试试看。 It will print 它会打印出来

I'm Son
Son

So what's going on here? 那么这里发生了什么? Your topmost constructor (that of Father ) calls an init() method, which is overridden in a subclass. 你最顶层的构造函数( Father )构造函数调用一个init()方法,该方法在子类中被重写。 As all constructor call super(); 由于所有构造函数调用super(); first, they are effectively executed superclass to subclass. 首先,它们有效地执行超类到子类。 So if the topmost constructor's first call is init(); 因此,如果最顶层的构造函数的第一个调用是init(); then all of the init happens before any constructor code. 然后所有的init都在任何构造函数代码之前发生。 If your init method fully initializes the object, then all constructors can work with an initialized object. 如果init方法完全初始化对象,则所有构造函数都可以使用初始化对象。 And since init() is polymorphic, it can even initialize the object when there are subclasses, unlike with the constructor. 由于init()是多态的,它甚至可以在存在子类时初始化对象,这与构造函数不同。

Note that init() is protected: subclasses will be able to call and override it, but classes in other package won't be able to call it. 请注意, init()受到保护:子类将能够调用并覆盖它,但其他包中的类将无法调用它。 That's a slight improvement over public and should be considered for x too. 这比public略有改善,也应考虑用于x

As other stated, you cannot override fields, you can only hide them. 如其他所述,您不能覆盖字段,您只能隐藏它们。 See JLS 8.3. JLS 8.3。 Field Declarations 现场声明

If the class declares a field with a certain name, then the declaration of that field is said to hide any and all accessible declarations of fields with the same name in superclasses, and superinterfaces of the class. 如果类声明了具有特定名称的字段,那么该字段的声明将被称为隐藏超类中具有相同名称的字段的任何和所有可访问声明,以及该类的超接口。

In this respect, hiding of fields differs from hiding of methods ( §8.4.8.3 ), for there is no distinction drawn between static and non-static fields in field hiding whereas a distinction is drawn between static and non-static methods in method hiding. 在这方面,隐藏字段不同于隐藏方法(第8.4.8.3节 ),因为在字段隐藏中静态和非静态字段之间没有区别,而在方法隐藏中区分静态和非静态方法。

A hidden field can be accessed by using a qualified name ( §6.5.6.2 ) if it is static, or by using a field access expression that contains the keyword super ( §15.11.2 ) or a cast to a superclass type. 如果是静态的,则可以使用限定名称(第6.5.6.2节 )访问隐藏字段,或者使用包含关键字super(第15.11.2节 )或转换为超类类型的字段访问表达式来访问隐藏字段。

In this respect, hiding of fields is similar to hiding of methods. 在这方面,隐藏字段类似于隐藏方法。

A class inherits from its direct superclass and direct superinterfaces all the non-private fields of the superclass and superinterfaces that are both accessible to code in the class and not hidden by a declaration in the class. 类继承自其直接超类和直接超接口超类和超接口的所有非私有字段,这些字段既可以访问类中的代码,也不会被类中的声明隐藏。

You can access Father 's hidden fields from Son 's scope using super keyword, but the opposite is impossible since Father class is not aware of its subclasses. 你可以使用super关键字从Son的范围访问Father的隐藏字段,但是相反的情况是不可能的,因为Father类不知道它的子类。

While methods can be overridden, attributes can be hidden. 虽然可以覆盖方法,但可以隐藏属性。

In your case, the attribute x is hidden: in your Son class, you can't access the Father 's x value unless you use the super keyword. 在您的情况下,属性x是隐藏的:在您的Son类中,除非使用super关键字,否则无法访问Fatherx值。 The Father class doesn't know about the Son 's x attribute. Father班不知道Sonx属性。

In the opposit, the toString() method is overriden: the implementation that will always be called is the one of the instantiated class (unless it does not override it), ie in your case Son , whatever the variable's type ( Object , Father ...). 在对立面中, toString()方法被覆盖:将始终被调用的实现是实例化的类之一(除非它不覆盖它),即在你的情况下Son ,无论变量的类型是什么( ObjectFather 。 ..)。

This is a behaviour done specially to have access to private members. 这是一种专门用于访问私有成员的行为。 So this.x looks at the variable X which is declared for Father, but when you pass this as a parameter to System.out.println in a method in Father - it looks at the method to call depending on the type of the parameter - in your case Son. 所以this.x查看为Father声明的变量X,但是当你将它作为参数传递给父中的方法中的System.out.println时 - 它会根据参数的类型查看要调用的方法 -在你的情况下儿子。

So how do you call the super classes method? 那么你如何调用超类方法呢? Using super.toString() , etc. 使用super.toString()

From Father it cannot access the x variable of Son. 从父亲来看,它无法访问Son的x变量。

Polymorphic method invocations apply only to instance methods. 多态方法调用仅适用于实例方法。 You can always refer to an object with a more general reference variable type ( a superclass or interface ), but at runtime, the ONLY things that are dynamically selected based on the actual object (rather than the reference type) are instance methods NOT STATIC METHODS. 您总是可以使用更通用的引用变量类型(超类或接口)引用对象,但在运行时,基于实际对象(而不是引用类型)动态选择的唯一事物是实例方法NOT STATIC METHODS 。 NOT VARIABLES . 不是变量 Only overridden instance methods are dynamically invoked based on the real object's type. 仅根据实际对象的类型动态调用重写的实例方法。

So variable x has not polymorphic behaviour because IT WILL NOT BE SELECTED DYNAMICALLY AT RUNTIME. 因此变量x没有多态行为,因为它不会在运行时动态选择。

Explaining your code : 解释你的代码:

System.out.println(this);

The Object type is Son so toString() method's Overridden Son version will be invoked. Object类型是Son因此将调用toString()方法的重写的Son版本。

System.out.println(this.x);

Object type is not in picture here, this.x is in Father class so x variable's Father version will be printed. 对象类型不在这里, this.xFather类中,因此将打印x变量的Father版本。

See more at: Polymorphism in java 请参阅: java中的多态性

This is commonly referred to as shadowing . 这通常被称为阴影 Note your class declarations: 注意你的类声明:

class Father {
    public String x = "Father";

and

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

This creates 2 distinct variables named x when you create an instance of Son . 当您创建Son的实例时,这将创建名为x 2个不同变量。 One x belongs to the Father superclass, and the second x belongs to the Son subclass. 一个x属于Father超类,第二个x属于Son子类。 Based on the output, we can see that when in the Father scope, this accesses the Father 's x instance variable. 根据输出,我们可以看到,在Father范围内, this访问Fatherx实例变量。 So the behavior is not related to "what this points to"; 所以行为与“ this指向什么”无关; it's a result of how the runtime searches for instance variables. 它是运行时搜索实例变量的结果。 It only goes up the class hierarchy to search for variables. 它只是上升的类层次结构来搜索变量。 A class can only reference variables from itself and its parent classes; 一个类只能引用自身及其父类的变量; it can't access variables from its child classes directly because it doesn't know anything about its children. 它无法直接从其子类访问变量,因为它对其子项没有任何了解。

To obtain the polymorphic behavior you want, you should only declare x in Father : 要获得所需的多态行为,您应该只在Father声明x

class Father {
    public String x;

    public Father() {
        this.x = "Father"
    }

and

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

This article discussed the behavior you're experiencing exactly: http://www.xyzws.com/Javafaq/what-is-variable-hiding-and-shadowing/15 . 本文讨论了您正在经历的行为: 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