Java SE 是什么,包括哪些内容(十八)?
本文内容参考自Java8标准
再次感谢Java编程思想对本文的启发!
我在之前的博文:JavaSE基础知识(十八)–Java多态之构造方法和多态https://blog.csdn.net/ruidianbaihuo/article/details/99703524 中提到了在继承结构中,有关组合类,父类,子类相互之间构造方法的调用顺序问题,实际上,这些构造方法调用的层次结构带来了一个有趣的两难问题:如果在一个构造方法的内部调用正在构造的对象的某个动态绑定方法,那会发生什么情况?(之前提到了,在Java中,如果没有特别声明,动态绑定是自动的,除非用static修饰,这种自动才会消失,所以这个问题就可以转化成:调用一个正在构造的对象的方法,会发生什么情况?实际上这不用问,肯定不行,因为这个对象还没有构造完毕,也就是还没有初始化完成。)
在一般的方法内部,动态绑定的调用是在运行时才决定的,因为对象无法知道它是属于方法所在的那个类,还是属于那个类的导出类。
如果在构造方法内部调用了一个动态绑定方法,可能会调用那个方法的被覆盖后的定义。然而,这个调用的效果与构造方法的调用顺序有很大的关系,因为被覆盖的方法在对象(实际是子类对象)被完全构造之前就被调用的话,这可能会造成一些难于发现的隐藏错误。
在这里很友情地提示:如果你正在看Java编程思想第四版此处的内容,我可以很负责任的告诉你,这段原文的翻译是我有史以来见过的最蹩脚的!我改良了一下,希望可以对你们有帮助。
从概念上讲,构造方法的实际工作是创建对象(这并非是一件平常的工作)。在任何构造方法内部,整个对象可能只是部分形成-----我们只知道父类对象已经进行初始化,如果构造方法只是在构建对象过程中的一个步骤,并且该对象所属的类是从这个构造方法所属的类导出的(子类),那么导出部分在当前构造方法正在被调用的时刻仍旧是没有被初始化的,然而,一个动态绑定的方法调用却会向外深入到继承层次结构内部,它可以调用导出类的方法。如果我们是在构造方法的内部这样做,那么就可能会调用某个方法,而这个方法所操纵的成员可能还未进行初始化,这肯定会造成问题。
如果上面这段话看不懂的话,没有关系,可以直接看下面的代码示例。然后再倒回来看。实际上上面这段话是很啰嗦的。
代码示例:
// 构造方法调用层次的问题
//类Glyph
class Glyph{
//方法draw()
void draw(){
//打印字符串"Glyph.draw()"
System.out.println("Glyph.draw()");
}
//构造方法
Glyph(){
//打印字符串"Glyph() before draw()"
System.out.println("Glyph() before draw()");
//问题就在这里,如果在运行的时候直接创建子类的对象,
//由于Java中的动态方法绑定,这里调用的一定是子类中
//覆盖了的方法,而不是父类自己的方法,而这个时候子类的构造方法
//还未被调用,子类的对象也还没创建,所以
//会出现问题。
draw();
//打印字符串"Glyph() after draw()"
System.out.println("Glyph() after draw()");
}
}
//类RoundGlyph继承类Glyph
class RoundGlyph extends Glyph{
//int类型的类变量radius,初始化值为1
private int radius = 1;
//构造方法,带一个int类型的形式参数r
RoundGlyph(int r){
//初始化类变量radius,值为实际传入的参数r的值
radius = r;
//打印字符串"RoundGlyph.RoundGlyph(),radius = " + radius
System.out.println("RoundGlyph.RoundGlyph(),radius = " + radius);
}
//覆盖父类中的方法draw()
void draw(){
//打印字符串"RoundGlyph.draw(),radius = " + radius
System.out.println("RoundGlyph.draw(),radius = " + radius);
}
}
//类PolyConstructors
public class PolyConstructors{
//程序执行入口main方法
public static void main(String[] args) {
//这段代码的分析要从这里开始分析,代码运行的时候
//是直接通过关键字new创建了一个匿名的子类RoundGlyph对象。
//根据前面说的构造方法的调用顺序,因为有继承,这个时候应该是先
//调用父类Glyph的构造方法,在父类构造方法体里有这么一句
//代码:draw();问题就在这里了,我们知道Java中方法的
//动态绑定是自动的,所以这里的这个draw()实际上不能调用父类
//的方法draw(),而是调用子类中被覆盖的方法draw(),但是这个时候
//子类的构造方法还未被调用,子类的对象还未被创建。
//所以在子类构造方法中的radius = r;还未来得及执行
//所以在这里初始化的值5还未赋值给radiu,自然radiu的值依然是0.
new RoundGlyph(5);
}
}
结果示例:
Glyph.draw()方法设计为将要被覆盖,这种覆盖是在RoundGlyph中发生的。但是Glyph构造方法会调用这个方法,结果导致了对RoungGlyph.draw()的调用,这看起来可能是我们的目的,但是如果能看到输出结果,我们会发现当Glyph的构造方法调用draw()方法时,radius不是默认的初始值1,而是0。这可能会导致在屏幕上只画了一个点,或者根本什么都没有,这个时候,你可能只能干瞪眼,并试图找出程序无法运转的原因所在。
从结果中可以看出,在调用父类的构造方法的时候,那个draw()方法确实因为Java中的动态绑定调用的是子类RoundGlyph里被覆盖的版本,但是因为这个时候变量radius的初始化操作在子类的构造方法里面,所以还未来得及执行(因为子类的构造方法调用顺序在父类的后面),所以,虽然在main()方法中直接传入了实际参数5,但是radius的值仍然是0,直到父类的构造方法执行完毕,调用了子类的构造方法,变量radius的初始化才执行,这个时候值才变成了5。
初始化的实际过程
①、在其他任何事物发生之前,将分配给对象的存储空间初始化成二进制的0。
②、如前所述那样调用父类的构造方法的时候,此时,调用被覆盖后的draw()方法(要在调用RoundGlyph构造器之前调用),由于步骤①的缘故,我们此时会发现radius的值为0。
③、按照声明的顺序调用成员的初始化方法。
④、调用子类的构造方法主体。
这样做有一个优点,那就是所有东西都至少初始化为零(或者是某些特殊数据类型中与零等价的值,比如布尔类型里的false),而不是仅仅留作垃圾。其中包括通过"组合"而嵌入一个类内部的对象引用,其值是null,所以如果忘记为该引用进行初始化,就会在运行是出现异常,查看输出结果时,会发现其它所有东西的值都会是零。这通常也是发现问题的证据。
另一方面,我们应该对这个程序的结果相当震惊,在逻辑方面,我们做的已经十分完美,,而它的行为却不可思议的错了,并且编译器也没有报错(在这种情况下,C++语言会产生更合理的行为),诸如此类的错误会很容易被人们忽略,而且可能要花很长的时间才能被发现。
因此,编写构造方法的时候有一条有效的准则:用尽可能简单的方法使对象进入正常的状态,如果可以的话,避免调用其它的方法,在构造方法内部唯一能够安全调用的哪些方法是父类中的final方法(也适用于private方法,他们自动疏忽final方法),这些方法不能被覆盖,因此也就不会出现上述令人惊讶的问题,虽然你可能无法总是能够遵循这条准则,但是应该朝着它努力。
PS:时间有限,有关Java SE的内容会持续更新!今天就先写这么多,如果有疑问或者有兴趣,可以加QQ:2649160693,并注明CSDN,我会就博文中有疑义的问题做出解答。同时希望博文中不正确的地方各位加以指正。