构造器和多态
通常,构造器不同于其他种类的方法。涉及到多态时仍旧时如此。尽管构造器并不具有多态性(他们实际上是static方法,只不过该static声明是隐式的)。
基类的构造器总是在导出类的构造过程中被调用,而且按照继承层次逐渐向上链接,确保每个基类的构造器都能得到调用。这样做是有意义的,因为构造器具有一项特殊任务:检查对象是否被正确地构造。导出类只能访问它自己的成员,不能访问基类中的成员。
下面这个例子,它展示组合、继承以及多态在构建顺序上的作用;
class Meal{
Meal(){
System.out.println("Meal()");
}
}
class Bread{
Bread(){
System.out.println("Bread()");
}
}
class Cheese{
Cheese(){
System.out.println("Cheese()");
}
}
class Lettuce{
//生菜
Lettuce(){
System.out.println("Lettuce()");
}
}
class Lunch extends Meal{
Lunch(){
System.out.println("Lunch()");
}
}
class ProtableLunch extends Lunch{
ProtableLunch(){
System.out.println("ProtableLunch()");
}
}
public class Sandwich extends ProtableLunch{
private Bread b = new Bread();
private Cheese c = new Cheese();
private Lettuce l = new Lettuce();
public Sandwich(){
System.out.println("Sandwich()");
}
public static void main(String[] args){
new Sandwich();
}
}
在这个例子中,用其他类创建了一个复杂的类,而且每个类都有一个声明它自己的构造器。其中最重要的类是Sandwich,它反映了三层继承以及三个成员对象。当在main()里创建一个Sandwich对象后,就可以看到输出结果。
这也表明了这一复杂对象调用构造器要遵照下面的顺序:
- 调用基类构造器。这个步骤会不断反复递归下去,首先是构造这种层次结构的根,然后是下一层导出类,等等,直到最底层的导出类
- 按声明顺序调用成员的初始化方法
- 调用导出类构造器的主题
8.3.3 构造器内部的多态方法的行为
构造器调用的层次结构带来了一个有趣的两难问题。如果在一个构造器内部调用正在构造的对象的某个动态绑定方法,会发生什么?
在一般的方法内部,动态绑定的调用是在运行时才决定的,因为对象无法直到它是属于方法所在的那个类,还是属于那个类的导出类。
从概念上讲,构造器的工作实际上是创建对象。在任何构造器内部,整个对象可能只是部分形成——我们只知道基类对象已经进行初始化。如果构造器只是在构建对象过程中的一个步骤,并且该对象所属的类是从这个构造器所属的类导出的。那么导出部分在当前构造器正在被调用的时刻仍旧是没有被初始化的。然而,一个动态绑定的方法调用却会向外深入到继承层次结构内部,它可以调用导出类里的方法。如果我们是在构造器内部这样做,那么可能会调用某个方法,而这个方法所操纵的成员可能还未进行初始化——这会导致灾难。
灾难:
class Glyph{
void draw(){
System.out.println("Glyph.draw()");
}
Glyph(){
System.out.println("Glyph() before draw()");
draw();
System.out.println("Glyph() after draw()");
}
}
class RoundGlyph extends Glyph{
private int radius = 1;
RoundGlyph(int r){
radius = r;
System.out.println("RoundGlyph.RoundGlyph() radius:" + radius);
}
void draw(){
System.out.println("RoundGlyph.draw() radius" + radius);
}
}
public class Polyconstructor {
public static void main(String[] args){
new RoundGlyph(5);
}
}
输出结果:
Glyph() before draw()
RoundGlyph.draw() radius0
Glyph() after draw()
RoundGlyph.RoundGlyph() radius:5
Glyph.draw()方法设计为将要被覆盖,这种覆盖实在RoundGlyph中发生的。但是Glyph构造器会调用这个方法,结果导致了对RoundGlyph.draw()的调用,但是看到输出结果,我们会发现当Glyph的构造器调用draw()方法时,radius不是默认初始值1而是0.
初始化的实际过程是:
- 在其他任何事物发生之前,将分配给对象的存储空间初始化成二进制的零
- 如前述那样调用基类构造器。此时,调用被覆盖后的draw()方法,由于步骤1的缘故,我们此时会发现radius的值为0。
- 按照声明的顺序调用成员的初始化方法。
- 调用导出类的构造器主体。
**编写构造器有一条有效的准则:用尽可能简单的方法使对象进入正常状态,如果可以的话,避免调用其他方法。**在构造器内唯一能够安全调用的那些方法是基类中的final方法,因为这些方法不能被覆盖。
8.5 用继承进行设计
当我们使用现成的类来建立新类时,如果首先考虑继承技术,反倒会加重我们的设计负担,使事情变得不必要的复杂起来。
更好的方式是首先选择“组合”,尤其是不能十分确定应该使用哪一种方式时。组合不会强制我们的程序设计进入继承的层次结构。而且,组合更加灵活,因为它可以动态选择类型;相反,继承在编译时就需要直到确切类型。
class Actor{
public void act(){}
}
class HappyActor extends Actor{
public void act(){
System.out.println("HappyActor");
}
}
class SadActor extends Actor{
public void act(){
System.out.println("SadActor");
}
}
class Stage{
private Actor actor = new HappyActor();
public void change(){
actor = new SadActor();
}
public void performplay(){
actor.act();
}
}
public class Transmogrify {
public static void main(String[] args){
Stage stage = new Stage();
stage.performplay();
stage.change();
stage.performplay();
}
}
输出结果
HappyActor
SadActor
在这里,Stage对象包含一个对Actor的引用,而Actor被初始化为HappyActor对象。这意味着performPlay()会产生某种特殊行为。既然引用在运行时可以与另一个不同的对象重新绑定起来,所以SadActor对象的引用可以在actor中被替代,然后由performPlay()产生的行为也随之改变。这样一来,在运行期间获得了动态灵活性。
一条通用的准则是:“用继承表达行为间的差异,并用字段表达状态上的变化”。在上述例子中,两者都用到了:通过继承得到了两个不同的类,用于表达act()方法的差异;而Stage通过运用组合使自己的状态发生变化。这种情况下,这种状态的改变也产生了行为的改变。
总结:
为了在自己的程序中有效地运用多态乃至面向对象技术,必须扩展自己的编程视野,使其不仅包括个别类的成员和信息,而且还要包括类与类之间的共同特性以及他们之间的关系。尽管这需要极大的努力,但这样做非常值得。
探讨构造器在继承和多态中的角色,分析构造器内部调用多态方法的潜在问题,以及如何有效设计构造器避免灾难。同时,比较继承与组合在设计模式中的应用。
133

被折叠的 条评论
为什么被折叠?



