28 December 2013

多数时候我们不希望重新编写代码,而是希望使用已有的代码并加以更改或添加,类的复用为我们提供了这种机制,这也是面向对象语言重要的特点之一。复用类的主要方法有两种,其一是组合,其二是继承。组合的主要特点是在新的类中中产生现有类的对象,即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