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

Gar*_*ett 27 java polymorphism inheritance this

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

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";
    }
}
Run Code Online (Sandbox Code Playgroud)

结果是

I'm Son
Father
Run Code Online (Sandbox Code Playgroud)

为什么"this"指向父构造函数中的Son,但"this.x"指向Father中的"x"字段."this"关键字如何运作?

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

Man*_*726 22

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


Sil*_*eak 13

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

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

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;
}
Run Code Online (Sandbox Code Playgroud)

重要的是第13,20和21行; 其他人代表System.out.println();自己,或隐含return;.aload_0加载this引用,getfield从对象中检索字段值,在本例中为from this.你在这里看到的是字段名称是合格的:Father.x.在一行中Son,您可以看到有一个单独的字段.但Son.x从未使用过; 只是Father.x.

现在,如果我们删除Son.x并添加此构造函数,该怎么办:

public Son() {
    x = "Son";
}
Run Code Online (Sandbox Code Playgroud)

首先看一下字节码:

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
}
Run Code Online (Sandbox Code Playgroud)

第4,5和7行看起来很好:this并且"Son"已加载,并且字段设置为putfield.为什么Son.x?因为JVM可以找到继承的字段.但重要的是要注意,即使该字段被引用Son.x,JVM实际发现的字段也是如此Father.x.

那么它能提供正确的输出吗?很不幸的是,不行:

I'm Son
Father
Run Code Online (Sandbox Code Playgroud)

原因是陈述的顺序.字节码中的第0行和第1行是隐式super();调用,因此语句的顺序如下:

System.out.println(this);
System.out.println(this.x);
x = "Son";
Run Code Online (Sandbox Code Playgroud)

当然会打印出来"Father".要摆脱这种情况,可以做一些事情.

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

有些人认为这是构造者本身概念的缺陷; 有些语言在这个意义上甚至不使用构造函数.您可以使用init()方法代替.在普通的方法,你有多态性的优势,所以你可以调用init()一个Father引用,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";
    }
}
Run Code Online (Sandbox Code Playgroud)

我没有它的名字,但尝试一下.它会打印出来

I'm Son
Son
Run Code Online (Sandbox Code Playgroud)

那么这里发生了什么?你最顶层的构造函数(那个Father)调用一个init()方法,该方法在子类中被重写.正如所有构造函数super();首先调用的那样,它们实际上是执行超类到子类.因此,如果最顶层的构造函数的第一个调用是,init();则所有init都在任何构造函数代码之前发生.如果init方法完全初始化对象,则所有构造函数都可以使用初始化对象.而且由于init()它是多态的,它甚至可以在有子类时初始化对象,这与构造函数不同.

请注意,init()受保护:子类将能够调用并覆盖它,但其他包中的类将无法调用它.这是一个小小的改进,public也应该考虑x.


sss*_*fff 7

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

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

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

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

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

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

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


sp0*_*00m 6

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

在你的情况下,属性x为隐藏:在你的Son类,你不能访问Fatherx,除非您使用值super的关键字.本Father类不知道有关Sonx属性.

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