从零学Java(9)——复用类
多数时候我们不希望重新编写代码,而是希望使用已有的代码并加以更改或添加,类的复用为我们提供了这种机制,这也是面向对象语言重要的特点之一。复用类的主要方法有两种,其一是组合,其二是继承。组合的主要特点是在新的类中中产生现有类的对象,即has-a 的关系。而继承则采用按照现有类的类型创建新类,并在其中添加新代码,即is-a的关系。
举一个组合的例子:
class A {
A() {}
public void f() {
System.out.println("A.f()");
}
}
class B {
private A obj = new A();
B() {
obj.f();
}
}
我们可以看到,类B中直接产生现有的类A,以此可以使用关于A的各种方法。
继承则是面向对象“出镜率”非常高的词汇,导出类通过继承基类来获得基类的成员和方法(public和protected)。事实上,创建一个新类时,都会继承Java的标准根类Object。Java使用extends来实现继承。同样举个例子:
class A {
A() {}
public void f() {
System.out.println("A.f()");
}
public void g() {
System.out.println("A.g()");
}
}
class B extends A {
B() {}
public void f() {
System.out.println("B.f()");
super.f();
}
public static void main(String[] args) {
B obj = new B();
obj.f();
obj.g();
}
}
上面的例子打印出的信息是:
B.f()
A.f()
A.g()
首先调用B类的f()方法时,首先打印出B.f(),其后调用基类(A类)的A.f(),这里super.f()的super指基类。其后,由于B类中没有g()方法,它会在基类中寻找这个方法并调用。为了继承,一般将数据成员指定为private,将方法指定为public。
初始化在任何语言中都很重要,而继承对初始化也有其要求。看看下面的例子:
class Art {
Art() {
System.out.println("Art constructor");
}
}
class Drawing extends Art {
Drawing() {
System.out.println("Drawing constructor");
}
}
public class Cartoon extends Drawing {
public Cartoon() {
System.out.println("Cartoon constructor");
}
public static void main(String[] args) {
Cartoon x = new Cartoon();
}
}
输出是:
Art constructor
Cartoon constructor
Drawing constructor
我们发现,当初始化Cartoon类时,其基类也被初始化。实际上,构建过程是从基类从里向外扩散的,也就是说,当一个类创建时,先找寻其基类的构造器,如果基类还继承了其他基类,则再向其基类寻找。当找到最里层的基类时开始构造,从里向外一层层构造,类似一个迭代过程。这也正是上篇所讲的,为什么基类的构造器设定为private型时,便无法被继承了。
这个过程是自动进行的。如果基类没有默认构造器或者带参数,则需要显式构造,构造方法则是上面提到过的super。
class Art {
Art(int i) {
System.out.println("Art constructor");
}
}
class Drawing extends Art {
Drawing(int i) {
super(i);
System.out.println("Drawing constructor");
}
}
讲完创建就要说说清理。清理与创建的顺序正好相反,它是从外向内的清理。
class Shape {
Shape(int i) { print("Shape constructor"); }
void dispose() { print("Shape dispose"); }
}
class Circle extends Shape {
Circle(int i) {
super(i);
print("Drawing Circle");
}
void dispose() {
print("Erasing Circle");
super.dispose();
}
}
它的清理过程是先执行导出类的清理代码,再执行基类的清除代码。这就像一棵大树,生长的时候要从根向叶子生长,但剪除的时候要从叶子向根剪除。如果一下子去掉了根,那么叶子你也就都找不到了。这种清理与以前说过的finalize方法不同,如果要执行这种清理工作,最好还是自己编写清理代码。
除了组合和继承,Java还提供了第三种可能——代理。代理是组合与继承的中庸之道,我们将一个成员对象置于所要构造的类中(类似组合),但与此同时我们在新类中暴露了该成员对象的所有方法(类似继承)。听起来有点“不明觉厉”,还是例子来的明显:
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 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);
}
}
首先我们先在SpaceShipDelegation类中创建SpaceShipControls类,这种处理使用了组合,但后面与组合不同的部分(更明确的说是特别的地方)在于,它为每一个基类中的方法都在导出类中创建了一个对应的方法,以此来调用这个基类方法,这样便把基类的所用方法都暴露了出来。
还有一个重要的概念——向上转型。
class Instrument {
public void play() {}
static void tune(Instrument i) {
i.play(); }
}
public class Wind extends Instrument {
public static void main(String[] args) {
Wind flute = new Wind();
Instrument.tune(flute); // Upcasting
}
}
例子中Wind类继承了基类Instrument, 并创建了Wind对象,但是Instrument类仍可以使用Wind对象。由于继承可以确保基类中所有的方法在导出类中也同样有效,所以能够向基类发送的所有信息同样也可以向导出类发送。Wind类继承自基类,它的方法要多于基类,故它向上转型是安全的。
讲了这么多方法,那么我们什么时候选择组合,什么时候选择继承,又在什么时候选择代理呢?组合技术通常用于想在新类中使用现有类的功能而非它的接口形式。即我们只想得到需要的功能,而不是现有类的所有接口。如果需要向上转型,则需要选择继承。如果即希望使用组合,又希望暴露现有类的所有接口,则使用“中庸”的组合。
blog comments powered by Disqus