在下面的代码段中,结果确实令人困惑。
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.x"指的是" x"父亲的领域。这是什么"这个"关键字工作?
我知道多态概念,但是[1]和[2]之间有不同之处?当新的Son()被触发时,内存中发生了什么?
答案 0 :(得分:21)
默认情况下,所有成员函数在Java中都是多态的。这意味着当你调用this.toString()时,Java使用动态绑定来解析调用,调用子版本。当您访问成员x时,您访问当前作用域的成员(父亲),因为成员不是多态的。
答案 1 :(得分: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;
}
重要的是第13,20和21行;其他人代表System.out.println();
本身或隐含return;
。 aload_0
加载this
引用,getfield
从对象中检索字段值,在本例中为this
。您在此处看到的是字段名称是合格的:Father.x
。在Son
的一行中,您可以看到有一个单独的字段。但Son.x
从未使用过;只有Father.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()
方法。在普通方法中,您具有多态性的优势,因此您可以在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";
}
}
我没有名字,但试试看。它会打印
I'm Son
Son
那么这里发生了什么?您最顶层的构造函数(Father
的)构造函数调用init()
方法,该方法在子类中被重写。正如所有构造函数首先调用super();
一样,它们实际上是执行超类到子类。因此,如果最顶层构造函数的第一次调用是init();
,则所有init都在任何构造函数代码之前发生。如果init方法完全初始化对象,则所有构造函数都可以使用初始化对象。由于init()
是多态的,它甚至可以在有子类时初始化对象,与构造函数不同。
请注意init()
受到保护:子类将能够调用并覆盖它,但其他包中的类无法调用它。这比public
略有改善,也应考虑x
。
答案 2 :(得分:7)
如其他所述,您无法覆盖字段,您只能隐藏它们。见JLS 8.3. Field Declarations
如果类声明了一个具有特定名称的字段,那么该字段的声明将被隐藏为隐藏超类中具有相同名称的字段的任何和所有可访问声明,以及该类的超接口。
在这方面,隐藏字段不同于隐藏方法(§8.4.8.3),因为在字段隐藏中静态字段和非静态字段之间没有区别,而静态字段和非静态字段之间存在区别。方法隐藏中的非静态方法。
如果隐藏字段是静态的,则可以使用限定名称(§6.5.6.2)访问隐藏字段,或者使用包含关键字super(§15.11.2)或强制转换的字段访问表达式来访问隐藏字段超类型。
在这方面,隐藏字段类似于隐藏方法。
一个类继承自其直接超类并直接超级接口超类和超接口的所有非私有字段,这些字段既可以访问类中的代码,也不会被类中的声明隐藏。
您可以使用Father
关键字访问Son
范围内的super
个隐藏字段,但由于Father
类,因此不可能相反我不知道它的子类。
答案 3 :(得分:6)
虽然可以覆盖方法,但可以隐藏属性。
在您的情况下,隐藏属性x
:在您的Son
课程中,除非您使用Father
,否则无法访问x
的{{1}}值{1}}关键字。 super
班级不了解Father
的{{1}}属性。
在对立面中,Son
方法被覆盖:将始终被调用的实现是实例化类之一(除非它不覆盖它),即在您的情况x
中,无论变量的类型是什么(toString()
,Son
...)。
答案 4 :(得分:2)
这是专门为访问私有成员而完成的行为。所以this.x查看为Father声明的变量X,但是当你在父的方法中将它作为参数传递给System.out.println
时,它会查看要调用的方法,具体取决于参数的类型 - 在你的情况下,儿子。
那么你如何调用超类方法呢?使用super.toString()
等
从父亲那里,它无法访问Son的x变量。
答案 5 :(得分:2)
多态方法调用仅适用于实例方法。您总是可以使用更通用的引用变量类型(超类或接口)引用对象,但在运行时,基于实际对象(而不是引用类型)动态选择的唯一事物是实例方法不是静态方法。不变量。只根据实际对象的类型动态调用重写的实例方法。
因此变量x
没有多态行为,因为它不会在运行时动态选择。
解释你的代码:
System.out.println(this);
对象类型为Son
,因此将调用toString()
方法的已覆盖Son
版本。
System.out.println(this.x);
此处的对象类型不在此处,this.x
位于Father
类中,因此x
变量&#39; s Father
版本将被打印。
详情请见:Polymorphism in java
答案 6 :(得分:1)
这通常被称为阴影。请注意您的类声明:
class Father {
public String x = "Father";
和
class Son extends Father {
public String x = "Son";
当您创建x
的实例时,这将创建名为Son
的2个不同的变量。一个x
属于Father
超类,第二个x
属于Son
子类。根据输出,我们可以看到在Father
范围内,this
访问Father
的{{1}}实例变量。所以行为与&#34; x
指向&#34;;它是运行时搜索实例变量的结果。只有向上类层次结构才能搜索变量。一个类只能引用自身及其父类的变量;它无法直接从其子类中访问变量,因为它对其子项没有任何了解。
要获得所需的多态行为,您应该仅在this
中声明x
:
Father
和
class Father {
public String x;
public Father() {
this.x = "Father"
}
本文讨论了您正在经历的行为:http://www.xyzws.com/Javafaq/what-is-variable-hiding-and-shadowing/15。