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

后端 未结 7 668
被撕碎了的回忆
被撕碎了的回忆 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  [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.

提交回复
热议问题