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

后端 未结 7 658
被撕碎了的回忆
被撕碎了的回忆 2021-02-02 09:23

In the below code snippet, the result is really confusing.

public class TestInheritance {
    public static void main(String[] args) {
        new Son();
                


        
相关标签:
7条回答
  • 2021-02-02 09:53

    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; the others represent the System.out.println(); itself, or the implicit return;. aload_0 loads the this reference, getfield retrieves a field value from an object, in this case, from this. What you see here is that the field name is qualified: Father.x. In the one line in Son, you can see there is a separate field. But Son.x is never used; only Father.x is.

    Now, what if we remove Son.x and instead add this constructor:

    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. Why Son.x? because the JVM can find the inherited field. 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.

    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(); 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". 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 printlns are the last statements in your constructor, your object is complete. 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. In ordinary methods, you have the advantage of polymorphism, so you can call init() on a Father reference, and Son.init() is invoked; whereas, new Father() always creates a Father object. (of course, in Java you still need to call the right constructor at some point).

    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. As all constructor call super(); first, they are effectively executed superclass to subclass. So if the topmost constructor's first call is init(); then all of the init happens before any constructor code. If your init method fully initializes the object, then all constructors can work with an initialized object. And since init() is polymorphic, it can even initialize the object when there are subclasses, unlike with the constructor.

    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. That's a slight improvement over public and should be considered for x too.

    0 讨论(0)
  • 2021-02-02 10:00

    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 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.

    Explaining your code :

    System.out.println(this);
    

    The Object type is Son so toString() method's Overridden Son version will be invoked.

    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.

    See more at: Polymorphism in java

    0 讨论(0)
  • 2021-02-02 10:06

    As other stated, you cannot override fields, you can only hide them. See 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.

    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.

    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.

    0 讨论(0)
  • 2021-02-02 10:08

    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.

    So how do you call the super classes method? Using super.toString(), etc.

    From Father it cannot access the x variable of Son.

    0 讨论(0)
  • 2021-02-02 10:09

    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. One x belongs to the Father superclass, and the second x belongs to the Son subclass. Based on the output, we can see that when in the Father scope, this accesses the Father's x instance variable. So the behavior is not related to "what this points to"; 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:

    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.

    0 讨论(0)
  • 2021-02-02 10:18

    All member functions are polymorphic in Java by default. That means when you call this.toString() Java uses dynamic binding to resolve the call, calling the child version. When you access the member x, you access the member of your current scope (the father) because members are not polymorphic.

    0 讨论(0)
提交回复
热议问题