Bootstrap

复用类(2):代理、结合使用组合和继承

1 代理

        第三种关系称为代理,这是继承与组合之间的中庸之道,因为我们将一个成员对象置于所要构造的类中(就像组合),但与此同时我们在新类中暴露了该成员对象的所有方法(就像继承)。例如,太空船需要一个控制模版:

/**
 * 宇宙飞船控制模版
 */
public class SpaceShipControls {
	void up(int velocity) {
	}

	void down(int velocity) {
	}

	void left(int velocity) {
	}

	void right(int velocity) {
	}

	// 向前
	void forward(int velocity) {
	}

	// 返回
	void back(int velocity) {
	}

	// 涡轮推动
	void turboBoost() {
	}
}

        构造太空船的一种方式是使用继承:

/**
 * 太空船
 */
public class SpaceShip extends SpaceShipControls {
	private String name;

	public SpaceShip(String name) {
		this.name = name;
	}

	public String toString() {
		return name;
	}

	public static void main(String[] args) {
		SpaceShip protector = new SpaceShip("NSEA protector");
		protector.forward(100);
	}

}

        然而,SpaceShip并非真正的SpaceShipControls类型,即便你可以“告诉”SpaceShip向前运动(forward())。更准确地讲,SpaceShip包含了SpaceShipControls,与此同时,SpaceShipControls的所有方法在SpaceShip中都暴露了出来。代理解决了此难题:

/**
 * 太空船代理
 */
public class SpaceShipDelegation {
	private String name;
	private SpaceShipControls controls = new SpaceShipControls();

	public SpaceShipDelegation(String name) {
		this.name = name;
	}

	// Delegated methods:
	public void back(int velocity) {
		controls.back(velocity);
	}

	public void down(int velocity) {
		controls.down(velocity);
	}

	public void forward(int velocity) {
		controls.forward(velocity);
	}

	public void left(int velocity) {
		controls.left(velocity);
	}

	public void right(int velocity) {
		controls.right(velocity);
	}

	public void turboBoost() {
		controls.turboBoost();
	}

	public void up(int velocity) {
		controls.up(velocity);
	}

	public static void main(String[] args) {
		SpaceShipDelegation protector = new SpaceShipDelegation("NSEA Protector");
		protector.forward(100);
	}
}

        可以看到,上面的方法是如何转递给了底层的controls对象,而其接口由此也就与使用继承得到的接口相同了。但是,我们使用代理时可以拥有更多的控制力,因为我们可以选择只提供在成员对象中的方法的某个子集。

2 结合使用组合和继承

        同时使用组合和继承是很常见的事。下例就展示了同时使用这两种技术,并配以必要的构造器初始化,来创建更加复杂的类:

/**
 * 盘
 */
class Plate {
	Plate(int i) {
		System.out.println("Plate constructor");
	}
}

/**
 * 餐盘
 */
class DinnerPlate extends Plate {
	DinnerPlate(int i) {
		super(i);
		System.out.println("DinnerPlate constructor");
	}
}

/**
 * 餐具
 */
class Utensil {
	Utensil(int i) {
		System.out.println("Utensil constructor");
	}
}

/**
 * 勺子
 */
class Spoon extends Utensil {
	Spoon(int i) {
		super(i);
		System.out.println("Spoon constructor");
	}
}

/**
 * 叉子
 */
class Fork extends Utensil {
	Fork(int i) {
		super(i);
		System.out.println("Fork constructor");
	}
}

/**
 * 刀具
 */
class Knife extends Utensil {
	Knife(int i) {
		super(i);
		System.out.println("Knife constructor");
	}
}

/**
 * 自定义
 */
class Custom {
	Custom(int i) {
		System.out.println("Custom constructor");
	}
}

/**
 * 餐位
 */
public class PlaceSetting extends Custom {
	private Spoon sp;
	private Fork frk;
	private Knife kn;
	private DinnerPlate pl;

	PlaceSetting(int i) {
		super(i + 1);
		sp = new Spoon(i + 2);
		frk = new Fork(i + 3);
		kn = new Knife(i + 4);
		pl = new DinnerPlate(i + 5);
		System.out.println("PlaceSetting constructor");
	}

	public static void main(String[] args) {
		PlaceSetting x = new PlaceSetting(9);
	}
}

        虽然编译器强制你去初始化基类,并且要求你要在构造器起始处就要这么做,但是它并不监督你必须将成员对象也初始化,因此在这一点上你自己必须时刻注意。

        这些类如此清晰地分离着实使人惊讶。甚至不需要这些方法的源代码就可以复用这些代码,我们至多只需要导入一个包。(对于继承与组合来说都是如此。)

3 确保正确清理

        java中没有C++中析构函数的概念。析构函数是一种在对象被销毁时可以被自动调用的函数。其原因可能是因为在java中,我们的习惯只是忘掉而不是销毁对象,并且让垃圾回收器在必要时释放其内存。

        通常这样做是好事,但有时类可能要在其声明周期内执行一些必需的清理活动。你并不知道垃圾回收器何时将会被调用,或者它是否将被调用。因此,如果你想要某个类清理一些东西,就必须显示地编写一个特殊方法来做这件事,并要确保客户端程序员知晓他们必须要调用这一方法。必须将这一清理工作置于finally子句之中,以预防异常的出现。

        请思考一下下面这个能在屏幕上绘制图案的计算机辅助设计系统示例:

/**
 * 形状
 */
class Shape {
	Shape(int i) {
		System.out.println("Shape constructor");
	}

	void dispose() {
		System.out.println("Shape dispose");
	}
}

/**
 * 圆形
 */
class Circle extends Shape {

	Circle(int i) {
		super(i);
		System.out.println("Drawing Circle");
	}

	void dispose() {
		System.out.println("Erasing Circle");
		super.dispose();
	}
}

/**
 * 三角形
 */
class Triangle extends Shape {

	Triangle(int i) {
		super(i);
		System.out.println("Drawing Triangle");
	}

	void dispose() {
		System.out.println("Erasing Triangle");
		super.dispose();
	}
}

/**
 * 线
 */
class Line extends Shape {
	private int start, end;

	Line(int start, int end) {
		super(start);
		this.start = start;
		this.end = end;
		System.out.println("Drawing Line: " + start + ", " + end);
	}

	void dispose() {
		System.out.println("Erasing Line: " + start + ", " + end);
		super.dispose();
	}
}

public class CADSystem extends Shape {

	private Circle c;
	private Triangle t;
	private Line[] lines = new Line[3];

	CADSystem(int i) {
		super(i + 1);
		for (int j = 0; j < lines.length; j++)
			lines[j] = new Line(j, j * j);
		c = new Circle(1);
		t = new Triangle(1);
		System.out.println("Combined constructor");
	}

	public void dispose() {
		System.out.println("CADSystem的dispose()");
		t.dispose();
		c.dispose();
		for (int i = lines.length - 1; i >= 0; i--)
			lines[i].dispose();
		super.dispose();
	}

	public static void main(String[] args) {
		CADSystem x = new CADSystem(47);
		try {
			// Code and exception handling...
		} finally {
			x.dispose();
		}
	}

}

        此系统中的一切都是某种Shape(Shape自身就是一种Object,因为Shape继承自根类Object)。每个类都覆写Shape的dispose()方法,并运用super来调用该方法的基类版本。尽管对象生命期中任何被调用的方法都可以做一些必需的清理工作,但是Circle、Triangle和Line这些特定的Shape类仍然都带有可以进行“绘制”的构造器。每个类都有自己的dispose()方法将存于内存之中的东西恢复到对象存在之前的状态。

        在main()中可以看到try和finally这两个关键字。关键字try表示,下面的块(用一组大括号括起来的范围)是所谓的保护区(guarded region),这意味着他需要被特殊处理。其中一项特殊处理就是无论try块是怎么退出的,保护区后的finally子句中的代码总是要被执行的。这里finally子句表示的是“无论发生什么事,一定要为x调用dispose()”。

        在清理方法(dispose())中,还必须注意对基类清理方法和成员对象清理方法的调用顺序,以防某个子对象依赖于另一个子对象情形的发生。一般而言,所采用的形式应该与C++编译器在其析构函数上所施加的形式相同:首先,执行类的所有特定的清理动作,其顺序同生成顺序相反(通常这就要求基类元素仍旧存活);然后,就如我们所示范的那样,调用基类的清理方法。

        许多情况下,清理并不是问题,仅需让垃圾回收器完成该动作就行。但当必须亲自处理清理时,就得多做努力并多加小心。因为,一旦涉及垃圾回收,能够信赖的事就不会很多了。垃圾回收器可能永远也无法被调用,即使被调用,它也可能以任何它想要的顺序来回收对象。最好的办法是除了内存以外,不能依赖垃圾回收器去做任何事。如果需要进行清理,最好是编写自己的清理方法,但不要使用finalize()。

4 名称屏蔽

        如果java的基类拥有某个已被多次重载的方法名称,那么在导出类中重新定义该方法名并不会屏蔽其在基类中的任何版本(这一点与C++不同)。因此,无论是在该层或者它的基类中对方法进行定义,重载机制都可以正常工作:

class Homer {
	char doh(char c) {
		System.out.println("doh(char)");
		return 'd';
	}

	float doh(float f) {
		System.out.println("doh(float)");
		return 1.0f;
	}
}

class Milhouse {
}

class Bart extends Homer {
	void doh(Milhouse m) {
		System.out.println("doh(Milhouse)");
	}
}

public class Hide {
	public static void main(String[] args) {
		Bart b = new Bart();
		b.doh(1);
		b.doh('x');
		b.doh(1.0f);
		b.doh(new Milhouse());
	}
}

        可以看到,虽然Bart引入了一个新的重载方法(C++中若要完成这项工作则需要屏蔽基类方法),但是在Bart中Homer的所有重载方法都是可用的。使用与基类完全相同的特征签名及返回类型来覆盖具有相同名称的方法,是一件极其平常的事。但它也令人迷惑不解。

        java SE5新增了@Override注解,它并不是关键字,但是可以把它当做关键字使用。当你想要覆写某个方法时,可以选择添加这个注解,在你不留心重载而并非覆写了该方法时,编译器就会生成一条错误消息:

class Lisa extends Homer {
	@Override
	void doh(Milhouse m) {
		System.out.println("doh(Milhouse)");
	}
}

        这样,@Override注解可以防止你在不想重载时而意外地进行了重载。  

;