有关继承情况下构造函数和初始化的运行机制问题(100分)

  • 主题发起人 主题发起人 Bkhswrp
  • 开始时间 开始时间
B

Bkhswrp

Unregistered / Unconfirmed
GUEST, unregistred user!
//: c06:Chess.java
// From 'Thinking in Java, 2nd ed.' by Bruce Eckel
// www.BruceEckel.com. See copyright notice in CopyRight.txt.
// Inheritance, constructors and arguments.
class BoardGame {
int m=100;
BoardGame(){
System.out.println("Haha,let's begin
:");
pp();
System.out.println("m = "+m);
}
void pp(){
System.out.println("pp 1"+"m = "+m);
}
}
public class Chess extends BoardGame {
int m=130;//(1)
//static int m=130;//(2)
Chess(String ss){
System.out.println(ss);
}
void pp(){
System.out.println("pp 2"+"m = "+m);
}
public static void main(String[] args) {
Chess y = new Chess("The end.
");
}
} ///:~
/*
问题1:很简单,成员函数可以重载,成员数据可不可以重载,是不是叫重载。
问题2:Chess y = new Chess("The end.
")调用后,首先执行的是BoardGame(),
BoardGame()里面有个pp(),真正执行的却是子类BoardGame重载的pp;BoardGame()
里面还有调用m的操作(System.out.println("m = "+m);),真正访问的m却是
父类定下的m;
问题3:另外一个奇怪的现象是因为子类和父类的pp都调用了m,而根据运行情况,
调用的应该是子类定下的m,但是上面的代码运行得到pp 2m = 0;把(1)语句去
掉换成(2)语句执行却得到pp 2m=130.我就纳闷了,很纳闷。
问题4:有谁能够透彻的讲一下继承情况下初始化和构造函数的运行机制?
*/
 
java里还真有点不太清楚,你可以仔细看看相关材料,delphi里必须是虚方法才能重载。
感觉java里这么写只是覆盖方法,并没有重载.也就是没有override而只是overload
 
这是Delphi程序吗?
发了才看到楼上的贴。
 
我这里没有jdk,你把执行结果贴出来。
 
运行结果如下
如果采用(1),则
Haha,let's begin
:
pp 2m = 0
m = 100 ///////////注意
The end.
如果采用(2),则
Haha,let's begin
:
pp 2m = 130 ///////////注意
m = 100
The end.
各位探讨一下吧。
 
是这样的,其实你如果在ide环境下单步跟踪一下就很清楚了。下面我来
给你分析一下这个结果吧:
首先,在java中没有c和delphi中的所谓虚函数的概念,java中所有的方法
都是在运行期连接的,也就是说,java中所有的方法都是虚的,即都是可以被
覆盖的。
那么,来看看这个结果,new一个类Chess,虽然在类Chess中你并没有声明
一个缺省的构造函数,也没有显式的调用父类boardgame的构造函数,但jdk
在运行时仍然会首先调用该类的缺省构造函数,这里,显然就是父类boardgame的
构造函数了。这已经解释了为什么第一行的结果是haha。。。。
接下来,在父类的构造函数中引用了pp这个方法,pp这个方法你没有加修饰符,
这说明它对于同一个包的类是可见的,显然,你的子类跟它在同一个包,所以,
子类的pp方法覆盖了父类的pp方法,那么,在父类中调用pp时,实际上,是通过
类本身的引用地址再加上方法的偏移地址来确定调用的入口的,记住,虽然现在
是在调用父类的一个方法,但这个类本身没有变化,仍然是chess类,它的基地址
再加上pp的偏移地址就是这次调用的入口,注意,这个pp指的是子类的pp方法,因
为在父类中虽然有该方法,但在子类的虚函数表中,该方法的地址记录的是子类的
pp方法的地址,这也就是所谓的覆盖。因此,输出的结果是p2 m=?。
我们再来看看为什么是m=0,而不是m=130或者100。首先,必须名确的是,子类的pp
方法中引用的m一定是子类定义的m,而不是父类的m,如果要引用父类的m,应该是
super.m,那么为什么不是130呢,因为此时jdk才仅仅开始构造父类,子类还没来得及
构造,也就说,m的值还没来得及初始化,那么,m的缺省初始化值是0,所以,输出为0。
那么后面一个m=100我想就不用我多说了吧。最后,回到chess类的构造方法中,系统
输出了the end。
再来看看第二个结果,不同的在于输出了p2 m=130,这是为什么?因为你定义的m是一个
静态成员变量,在java中,静态成员变量总是最先被初始化的,这就是原因。
还是多说一句,在父类中的m=100这句,父类是不能引用子类的成员变量的,因此,这里
引用的就是父类的成员变量。
最后,再提醒你一下,你的m和pp前面都没有加任何修饰符,这种情况在java中并没有一个
确定的称呼,它的访问权限类似于c++中的友元,但又有些不同,按照sun的官方建议,
除非在特别需要的情况下,不要使用这种缺省的访问权限定义,而是显式的使用public,
private,protected,以避免一些不必要的混淆和误会。
 
补充一下,对于pp方法的调用,我的说明是没有任何问题,但
对于m的调用,我不能确信你的运行结果是正确的,我只是根据你
给出的结果作出了解释,在你的结果是确定的情况下,那么我的
解释无疑是正确的,否则我的解释可能会有些小的缪误,因为我
很少使用这种缺省的权限定义,如果你明确的定义了访问权限的话,
我倒是可以给你一个确定的解释[8D]
 
那么为什么不是130呢,因为此时jdk才仅仅开始构造父类,子类还没来得及
构造,也就说,m的值还没来得及初始化,那么,m的缺省初始化值是0,所以,输出为0。
这句话是错误的。
Java装载Chess类的过程是:
1,装载Chess,解析其符号引用.比如m,pp等.
2,在解析符号引用的时候发现类Chess的直接父类BoardGame,于是装载BoardGame并且继续解析BoardGame的父类,直到Object类.
3,执行类Chess的<clinit>()方法.这个方法用来初始化所有非编译时可以决定的类变量的值.
比如说,假如你定义private static int m = (int) (Math.random() * 2.99),那么这个<clinit>()方法会执行这个计算.
(但是这仅限于类变量)
4,执行Chess的main()方法,其中创建新的Chess()实例.
5,在执行Chess()的构造方法之前,先初始化Chess类(触发对BoardGame类的初始化)然后执行BoardGame类的构造方法.
注意,此时Chess的所有实例变量都是在初始化时置的默认值,而没有进行设置值的操作.
6,执行BoardGame()构造方法之后,在执行Chess构造方法之前,对Chess的实例变量进行设置值的操作.
7,执行Chess构造方法的内容.
要确认这个事情,你可以这样:
public class Chess extends BoardGame {
int n= initN();
//(1)
//static int m=130;//(2)
Chess(String ss){

System.out.println(n);
System.out.println(ss);
}
void pp(){
System.out.println("pp 2"+"n = "+n);
}

int initN()
{
System.out.println("initN");
return 12;
}
public static void main(String[] args) {
Chess y = new Chess("The end.
");
}
}
结果:
Haha,let's begin
:
pp 2n = 0
m = 100
initN
12
The end.
深入的分析可以参见:http://www.artima.com/insidejvm/ed2/linkmod14.html
这本书是深入Java虚拟机 第二版。
即将由机械工业出版社翻译出版,敬请关注.
 
总算给你一个打广告的机会了...
我的错误是错误的使用了初始化这个词,我的本意是指没有设定值。
 
这好像是个anti-pattern啊,正好说明了由于继承带来的问题,由于超类中有self-use,
所以继承时,子类一旦重载超类中被自调用的方法,子类就可能会失败
 
多人接受答案了。
 
后退
顶部