构造器和多态

来源:互联网 发布:mac如何打罗马数字 编辑:程序博客网 时间:2024/05/02 02:01

引言
通常,构造器不同于其他种类的方法。涉及到多态时仍是如此。尽管构造器并不具备多态性(因为他们实际上是static方法,只不过该static声明是隐式的),但还是非常有必要理解构造器怎样通过多态在复杂的层次机构中运作的。


目录

  1. 构造器的调用顺序
  2. 继承和清理
  3. 构造器内部的多态方法的行为
  4. 总结

1、构造器的调用顺序

基类的构造器总是在导出类的构造过程中被调用,而且按照继承层次逐渐向上链接每个基类的构造器都能得到调用。这样做是有意义的,因为构造器具有意向特殊的任务:检查对象是否被正确构造。导出类只能访问它自己的成员,不能访问基类中的成员(基类成员是private类型)。只有基类的构造器才具有恰当的知识和权限来对自己的元素进行初始化。因此必须令所有的构造器都得到调用,否则就不能正确构造完整对象。这正是编译器为什么强制每个导出类部分都必须调用构造器的原因。在导出类的构造器主体中,如果没有明确指定某个基类的构造器,它就会“默认”地调用默认构造器。如果不存在默认构造器,编译器就会报错(若某个类型没有构造器,编译器会自动合成出一个默认构造器)。
下面的例子,它展示组合、继承以及多态在构建顺序上的作用:

package polymorphism.sandwich;//:polymorphism/sandwich/Sandwich.java//Order of constructor calls.import static net.mindview.util.Print.*;class Meal{    Meal(){        print("Meal()");    }}class Bread{    Bread(){        print("Bread()");    }}class Cheese{    Cheese(){        print("Cheese()");    }}class Lettuce{    Lettuce(){        print("Lettuce()");    }}class Lunch extends Meal{    Lunch(){        print("Lunch()");    }}class PortableLunch extends Lunch{    PortableLunch(){        print("PortableLunc()");    }}public class Sandwich extends PortableLunch {    private Bread b=new Bread();    private Cheese c=new Cheese();    private Lettuce l=new Lettuce();    public Sandwich(){        print("Sandwich()");    }    public static void main(String[] args) {        // TODO Auto-generated method stub        new Sandwich();    }}///:~/* *  * Meal() * Lunch() * PortableLunc() * Bread() * Cheese() * Lettuce() * Sandwich() *  */

类图:
这里写图片描述


说明:
在这个例子中,用其他类创建了一个复杂的类,而且每个类都有一个声明它自己的构造器。其中最重要的类是Sandwich,它反映了三层继承(若将自Object的隐含继承也算在内,就是四层)以及三个成员对象。当在main()里创建一个Sandwich对象后,就可以看到输出结果。
这也表明了这一复杂对象调用构造器要遵循下面的顺序:

  1. 调用基类构造器。这个步骤会不断地反复递归下去,首先构造这种层次结构的根,然后是下一层导出类,等等,直到最底层的导出类。
  2. 按声明顺序调用成员的初始化方法
  3. 调用导出类构造器的主体

构造器的调用顺序是很重要的。当进行继承时,我们已经知道基类的一切,并且可以访问基类中任何声明为public和protected的成员。这意味着在导出类中,必须假定基类的所有成员都是有效的。一种标准方法是,构造动作一经发生,那么对象所有部分的全体成员都会得到构建。然而,在构造器内部,我们必须确保所要使用的成员都已经构建完毕。为确保这一个目的,唯一的方法就是首先调用基类构造器。那么在进入导出类构造器时,在基类中可供我们访问的成员都已得到初始化。此外,知道构造器中所有成员都有效也是因为,当成员对象在类内进行定义的时候(比如上例中的b,c和l)只要有可能,就应该对他们进行初始化(也就是说,通过组合方法将对象置于类内)。若要遵循这一规则,那么就能保证所有基类成员以及当前对象的成员对象都被初始化了。但遗憾的是,这种做法并不适合于所有情况,这一点在下一节介绍。


2、继承与清理

通过组合和继承方法来创建新类时,永远不必担心对象的清理问题,子对象通过都会留给垃圾回收器进行处理。如果确实遇到清理的问题,那么必须用心为新类创建dispose()方法(在这里我选用此名称,读者可以提出更好的名字)。并且由于继承的缘故,如果我们有其他作为垃圾回收一部分的特殊清理动作,就必须在导出类中覆盖dispose()方法。当覆盖被继承类的dispose()方法时,务必记住调用基类版本dispose()方法;否则,基类的清理动作就不会发生。
下面证明这一点:

package polymorphism.frog;//:polymorphism/frog/Frog.java//Cleanup and inheritanceimport static net.mindview.util.Print.*;class Characteristic{    private String s;    Characteristic(String s){        this.s=s;        print("Creating Characteristic "+s);    }    protected void dispose(){        print("disposing Characteristic "+s);    }}class Description{    private String s;    Description(String s){        this.s=s;        print("Creating Description "+s);    }    protected void dispose(){        print("disposing Decription "+s);    }}class LivingCreature{    private Characteristic p=new Characteristic("is alive");    private Description t=new Description("Basic Living Creature");    LivingCreature(){        print("LivingCreature()");    }    protected void dispose(){        print("LivingCreature dispose");        t.dispose();        p.dispose();    }}class Animal extends LivingCreature{    private Characteristic p=new Characteristic("has heart.");    private Description t=new Description("Animal is not vegetable.");    Animal(){        print("Animal()");    }    protected void dispose(){        print("Animal dispose.");        t.dispose();        p.dispose();        super.dispose();    }}class Amphibian extends Animal{    private Characteristic p=new Characteristic("can live in water.");    private Description t=new Description("Both water and land.");    Amphibian(){        print("Amphibian()");    }    protected void dispose(){        print("Amphibian dispose.");        t.dispose();        p.dispose();        super.dispose();    }}public class Frog  extends Amphibian{    private Characteristic p=new Characteristic("Croaks");    private Description t=new Description("Eats Bugs");    Frog(){        print("Frog()");    }    protected void dispose(){        print("Frog dispose.");        t.dispose();        p.dispose();        super.dispose();    }    public static void main(String[] args) {        // TODO Auto-generated method stub        Frog frog=new Frog();        print("Bye!");        frog.dispose();     }}///:~/* * Creating Characteristic is aliveCreating Description Basic Living CreatureLivingCreature()Creating Characteristic has heart.Creating Description Animal is not vegetable.Animal()Creating Characteristic can live in water.Creating Description Both water and land.Amphibian()Creating Characteristic CroaksCreating Description Eats BugsFrog()Bye!Frog dispose.disposing Decription Eats Bugsdisposing Characteristic CroaksAmphibian dispose.disposing Decription Both water and land.disposing Characteristic can live in water.Animal dispose.disposing Decription Animal is not vegetable.disposing Characteristic has heart.LivingCreature disposedisposing Decription Basic Living Creaturedisposing Characteristic is alive *  */

类图:
这里写图片描述


说明:
层次结构中的每个类都包含Characteristic和Description这两种类型的成员对象,并且他们也必须销毁。所以万一某个对象要依赖于其他对象,销毁的顺序应该和初始化顺序相反。对于字段,则意味着与声明的顺序相反(因为字段的初始化是按照声明的顺序进行的)。对于基类(遵循C++中析构函数的形式),应该首先对其导出类进行清理,然后才是基类。这是因为导出类的清理可能会调用基类中的某些方法,所有需要使基类中的构件仍起作用而不应该过早地销毁他们。从输出中可以看出,Frog对象的所有部分都是按照创建的逆序进行销毁的。


在上面的实例示例中还应该注意到,Frog对象拥有其自己的成员对象。Frog对象创建了它自己的成员对象,并且知道它们应该存活多久(只要Frog存活着),因此Frog对象知道何时调用dispose()方法去释放其成员对象。然而,如果这些成员对象中存在于其他一个或多个对象共享的情况,问题就变得更加复杂了,你就不能简单地假设你可以调用dispose()了。在这种情况下,也许必须使用引用计数来跟踪仍旧访问着共享对象的对象数量了。下面是相关代码:

package polymorphism.referencecounting;//:polymorphism/referencecounting/ReferenceCounting.java//clean up shared member objects.import static net.mindview.util.Print.*;class Shared{    private int refcount=0;    private static long counter=0;    private final long id=counter++;    Shared(){        print("Creating "+this);    }    public void addRef(){        refcount++;    }    protected void dispose(){        if(--refcount==0){            print("Disposing " +this);        }    }    public String toString(){        return "Shared "+id;    }}class Composing{    private Shared shared;    private static long counter=0;    private final long id=counter++;    public Composing(Shared shared){        print("Creating "+this);        this.shared=shared;        this.shared.addRef();    }    protected void dispose(){        print("disposing "+this);        shared.dispose();        }    public String toString(){        return "Composing "+id;    }}public class ReferenceCounting {    public static void main(String[] args) {        // TODO Auto-generated method stub        Shared shared=new Shared();        Composing[] composing={                new Composing(shared),                new Composing(shared),                new Composing(shared),                new Composing(shared),                new Composing(shared),        };        for(Composing c:composing){            c.dispose();        }    }}///:~/* * Creating Shared 0Creating Composing 0Creating Composing 1Creating Composing 2Creating Composing 3Creating Composing 4disposing Composing 0disposing Composing 1disposing Composing 2disposing Composing 3disposing Composing 4Disposing Shared 0**/

说明:
static long counter跟踪所创建的Share的实例的数量,还可以为id提供数值。counter的类型是long而不是int,这样可以防止溢出(这只是一个良好的实践)id是final的,因为我们不希望他的值在对象生命周期中被改变。
在将一个共享对象附着到类上时,必须记住调用addRef(),但是dispose()方法将跟踪引用并决定何时执行清理。使用这种技巧需要加倍地细心。但是如果你正在共享需要清理的对象时,那么你就没有太多的选择余地了。


3、构造器内部的多态方法的行为

构造器调用的层次结构带来了一个有趣的两难问题。如果在一个构造器的内部调用正在构造的对象的某个动态绑定方法,那会发生什么情况?
在一般的方法内部,动态绑定的调用是在运行时才决定的,因为对象无法知道它是属于方法所在的那个类,还是属于那个类的导出类。
如果要调用构造器内部的一个动态绑定方法,就要用到那个方法的被覆盖后的定义。然而,这个调用的效果可能相当难于预料,因为覆盖的方法在对象被完全构造之前就会被调用。这可能会造成一些难于发现的隐藏错误。
从概念上讲,构造器的工作实际上是创建对象(这并非是一件平常的工作)。在任何构造器内部,整个对象可能只是部分形成——我们知道基类对象已经进行初始化。如果构造器只是在构建对象过程中的一个步骤,并且该对象所属的类是从这个构造器所属的类导出的,那么导出部分在当前构造器正在被调用的时刻仍旧是没有被初始化的。然而,一个动态绑定的方法调用却会向外深入到继承层次结构内部,他可以调用导出类里的方法。如果我们是在构造器内部这样做,那么就可能会调用某个方法,而这个方法所操作的成员可能还未进行初始化——这肯定会招致灾难。


通过下面的例子,我们看到问题所在:

package polymorphism.polyconstructor;//:polymorphism/polyconstructor/PolyConstrustors.java//Constructors and polymorphism //don't produce what you might expect.import static net.mindview.util.Print.*;class Glyph{    void draw(){        print("Glyph.draw()");    }    Glyph(){        print("Glyph() before draw()");        draw();        print("Glyph() after draw()");    }}class RoundGlyph extends Glyph{    private int radius=1;    RoundGlyph(int r){        radius=r;        print("RoundGlyph.RoundGlyph(), radius = "+radius);    }    void draw(){        print("RoundGlyph.draw(), radius = "+radius);    }}public class PolyConstructors {    public static void main(String[] args) {        // TODO Auto-generated method stub        new RoundGlyph(5);    }}///:~/* * Glyph() before draw()RoundGlyph.draw(), radius = 0Glyph() after draw()RoundGlyph.RoundGlyph(), radius = 5**/

Glyph.draw()方法设计为将要被覆盖,这种覆盖是在RoundGlyph中发生的。但是Glyph构造器会调用这个方法,结果导致了对RoundGlyph.draw()的调用,这看起来似乎是我们的目的。但是如果看到输出结果,我们会发现当Glyph的构造器调用draw()方法时,radius不是默认初始值1,而是0。这可能导致在屏幕上只画了一个点,或者根本什么东西都没有。


4、总结

之前的初始化顺序并不完整,而这正是解决这个谜题的关键所在。初始化的实际过程是:

  1. 在其他任何事物发生之前,将分配给对象的存储空间初始化成二进制的零。
  2. 如前所述那样调用基类构造器。此时,调用被覆盖后的draw()方法(要在调用RoundGlyph构造器之前调用),由于步骤一的缘故,我们此时会发现radius的值为0.
  3. 按声明顺序调用成员的初始化方法。
  4. 调用导出类的构造器主体。

因此,编写构造器时有一条有效的准则:“用尽可能简单的那些方法是是对象进入正常状态;如果可以的话,避免调用其他方法”。在构造器内唯一能够安全调用的那些方法是基类中的final方法(也适用于private方法,他们自动属于final方法)。这些方法不能被覆盖,因此也就不会出现上述令人吃惊的问题。
你可能无法总是能够遵循这条准则,但是应该朝着它努力!

0 0