多态(3):构造器和多态

    通常,构造器不同与其他种类方法。涉及到多态时仍是如此。尽管构造器并不具有多态性(它们实际上是static方法,只不过该static声明是隐式的),但还是非常有必要理解构造器怎样通过多态在复杂的层次结构中运作,这一理解将有助于大家避免一些令人不快的困扰。

一、构造器的调用顺序

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

    让我们来看下面这个例子,他展示组合,继承以及多态在构建顺序上的作用:

/**
 * 饭
 */
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 PortableLunch extends Lunch {
	PortableLunch() {
		System.out.println("PortableLunch()");
	}
}

/**
 * 三明治
 */
public class Sandwich extends PortableLunch {
	private Bread b = new Bread();
	private Cheese c = new Cheese();
	private Lettuce l = new Lettuce();

	Sandwich() {
		System.out.println("Sandwich()");
	}

	public static void main(String[] args) {
		new Sandwich();
	}
}

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

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

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

二、继承与清理

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

/**
 * 特征
 */
class Characteristic {
	private String s;

	Characteristic(String s) {
		this.s = s;
		System.out.println("Creating Characteristic " + s);
	}

	protected void dispose() {
		System.out.println("disposing Characteristic " + s);
	}
}

/**
 * 描述
 */
class Description {
	private String s;

	Description(String s) {
		this.s = s;
		System.out.println("Creating Description " + s);
	}

	protected void dispose() {
		System.out.println("disposing Description " + s);
	}
}

/**
 * 生物
 */
class LivingCreature {
	private Characteristic p = new Characteristic("is alive");
	private Description t = new Description("Basic Living Creature");

	LivingCreature() {
		System.out.println("LivingCreature()");
	}

	protected void dispose() {
		System.out.println("LivingCreature dispose");
		t.dispose();
		p.dispose();
	}
}

/**
 * 动物
 */
class Animal extends LivingCreature {
	private Characteristic p = new Characteristic("has heart");
	private Description t = new Description("Animal not Vegetable");

	Animal() {
		System.out.println("Animal()");
	}

	protected void dispose() {
		System.out.println("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() {
		System.out.println("Amphibian()");
	}

	protected void dispose() {
		System.out.println("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");

	public Frog() {
		System.out.println("Frog()");
	}

	protected void dispose() {
		System.out.println("Frog dispose");
		t.dispose();
		p.dispose();
		super.dispose();
	}

	public static void main(String[] args) {
		Frog frog = new Frog();
		System.out.println("Bye!");
		frog.dispose();
	}
}

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

    在这个例子中可以看到,尽管通常不必执行清理工作,但是一旦选择要执行,就必须谨慎和小心。

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

/**
 * 共享
 */
class Shared {
	private int refcount = 0;
	private static long counter = 0;
	private final long id = counter++;

	public Shared() {
		System.out.println("Creating " + this);
	}

	public void addRef() {
		refcount++;
	}

	protected void dispose() {
		if (--refcount == 0)
			System.out.println("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) {
		System.out.println("Creating " + this);
		this.shared = shared;
		this.shared.addRef();
	}

	protected void dispose() {
		System.out.println("disposing " + this);
		shared.dispose();
	}

	public String toString() {
		return "Composing " + id;
	}
}

public class ReferenceCounting {
	public static void main(String[] args) {
		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();
	}
}

    static long counter跟踪所创建的Shared的实例的数量,还可以为id提供数值。counter的类型是long而不是int,这样可以防止溢出。id是final的,因为我们不希望它的值在对象声明周期中被改变。

    在将一个共享对象附着到类上时,必须记住调用addRef(),但是dispose()方法将跟踪引用数,并决定何时执行清理。使用这种技巧需要加倍地细心,但是如果你正在共享需要清理的对象,那么你就没有太多的选择余地了。

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

    构造器调用的层次结构带来了一个有趣的两难问题。如果在一个构造器的内部调用正在构造的对象的某个动态绑定方法,那会发生什么情况呢?

    在一般的方法内部,动态绑定的调用是在运行时才决定的,因为对象无法知道它是属于方法所在的那个类,还是属于那个类的导出类。

    如果要调用构造器内部的一个动态绑定方法,就要用到那个方法的被覆盖后的定义。然而,这个调用的效果可能相当难于预料,因为被覆盖的方法在对象被完全构造之前就会被调用。这可能会造成一些难于发现的隐藏错误。

    从概念上讲,构造器的工作实际上是创建对象(这并非是一件平常的工作)。在任何构造器内部,整个对象可能只是部分形成--我们只知道基类对象已经进行初始化。如果构造器只是在构建对象过程中的一个步骤,并且该对象所属的类是从这个构造器所属的类导出的,那么导出部分在当前构造器正在被调用的时刻仍旧是没有被初始化的。然而,一个动态绑定的方法调用却会向外深入到继承层次结构内部,它可以调用导出类里的方法。如果我们是在构造器内部这样做,那么就可能会调用某个方法,而这个方法所操纵的成员可能还未进行初始化--这肯定会招致灾难。

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

/**
 * 字形
 */
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 PolyConstructors {
	public static void main(String[] args) {
		new RoundGlyph(5);
	}
}

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

    前一节讲述的初始化顺序并不十分完整,而这正是解决这一谜题的关键所在。初始化的实际过程是:

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

    这样做有一个优点,那就是所有东西都至少初始化成零(或者是某些特殊数据类型中与“零”等价的值),而不是仅仅留作垃圾。其中包括通过“组合”而嵌入一个类内部的对象引用,其值是null。所以如果忘记为该引用进行初始化,就会在运行时出现异常。查看输出结果时,会发现其他所有东西的值都会是零,这通常也正是发现问题的证据。

    另一方面,我们应该对这个程序的结果相当震惊。在逻辑方面,我们做的已经十分完美,而它的行为却不可思议地错了,并且编译器也没有报错。诸如此类的错误会很容易被人忽略,而且要花很长的时间才能发现。

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

如果本文对您有很大的帮助,还请点赞关注一下。

发布了100 篇原创文章 · 获赞 2 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/qq_40298351/article/details/104165617