10 January 2014

多态似乎就是伴随着面向对象编程产生的,就像“继承”一样,多态必定也要体现其不可替代的重要性。也许读过了这么多书籍,看过这么多文字,说不定练习的代码行数都不少了,我还是要问,到底什么是多态(请始终记得这个问题,这将是本篇博文的重要线索)。书上说:

多态是继数据抽象和继承(”继承“再次不期而遇)之后的第三种基本特征。通过分离做什么和怎么做,从另一角度将接口和实现分离开来。多态不但能够改善代码的组织结构和可读性,还能够创建可扩展的程序。多态的作用是消除类型之间的耦合。

从上面的文字我们至少能得出两个结论:多态能够将接口和实现分离,多态消除类型之间的耦合。这些结论都是站在比较高的角度着眼,对于我们这些“新来的”,抽象的难易理解。我们直接拿一段代码来解释:

enum Note {
    MIDDLE_C, C_SHARP, B_FLAT; // Etc.
}
class Instrument {
    void play(Note n) { print("Instrument.play() " + n); }
    String what() { return "Instrument"; }
    void adjust() { print("Adjusting Instrument"); }
}

class Wind extends Instrument {
    void play(Note n) { print("Wind.play() " + n); }
    String what() { return "Wind"; }
    void adjust() { print("Adjusting Wind"); }
}

class Percussion extends Instrument {
    void play(Note n) { print("Percussion.play() " + n); }
    String what() { return "Percussion"; }
    void adjust() { print("Adjusting Percussion"); }
}

class Stringed extends Instrument {
    void play(Note n) { print("Stringed.play() " + n); }
    String what() { return "Stringed"; }
    void adjust() { print("Adjusting Stringed"); }
}

class Brass extends Wind {
    void play(Note n) { print("Brass.play() " + n); }
    void adjust() { print("Adjusting Brass"); }
}

class Woodwind extends Wind {
    void play(Note n) { print("Woodwind.play() " + n); }
    String what() { return "Woodwind"; }
}

public class Music3 {
    public static void tune(Instrument i) {
        i.play(Note.MIDDLE_C);
    }
    public static void tuneAll(Instrument[] e) {
        for(Instrument i : e)
            tune(i);
    }
    public static void main(String[] args) {
        Instrument[] orchestra = {
            new Wind(),
            new Percussion(),
            new Stringed(),
            new Brass(),
            new Woodwind()
        };
        tuneAll(orchestra);
    }
}

输出:

Wind.play() MIDDLE_C
Percussion.play() MIDDLE_C
Stringed.play() MIDDLE_C
Brass.play() MIDDLE_C
Woodwind.play() MIDDLE_C

代码真的有点长,不过我们只需要关注一些重要的地方即可,很多细节我们可以省略。Wind、Percussion、Stringed、Brass、Woodwind都是直接或间接继承基类Instrument,且它们都有属于自己的play方法。那么我们先从main函数看起,首先它创建了一个Instrument数组引用,并进行了初始化,初始化元素的类型既是前面提到的五种类型对象。这里也许你会有疑惑,其实它使用了“向上转型”,我在这篇《从零学Java(9)——复用类》中对向上转型做了解释,事实上,向上转型就是多态能够实现的关键技术。举个例子,我们会把各种水果放在一个专门装水果的盒子里,当我们从盒子里拿出某类水果时,我们会根据具体拿出来的水果种类进行判断。

随后调用tuneAll,遍历整个数组,每个元素都要调用tune方法处理。具体看tune方法,它实际上调用了play这个方法。那具体调用的是哪个对象的play方法?这就要靠多态来解决了。还是刚才的例子,我们从水果盒子里拿出来某个特定类的水果,根据具体水果类型判断后,我们就知道拿出来的水果是什么类型(类)的,它有什么特性(方法或成员)。多态使得每个Instrument(以其为基类的对象)都能找到各自属于的具体导出类,并调用它们自己的play方法。如果你看过我写的《从零学Java(1)——面向对象编程》,那时我说的“神奇的代码”的原理在此得到了解释。

这有什么了不起的(似乎智能了一点),如果我们不用多态该如何实现?

public class Music3 {
    public static void tune(Wind i) {
        i.play(Note.MIDDLE_C);
    }
    public static void tune(Percussion i) {
        i.play(Note.MIDDLE_C);
    }
    public static void main(String[] args) {
        Wind w = new Wind();
        tune(w);
        Percussion p = new Percussion();
        tune(p);
        ...
    }
}

五种类型大概要重写成若干行代码(包括重载方法),上面给出其中两种类。首先,这我们每添加一个新的类(继承Instrument类),就要编写特定的方法,例如这个tune方法,就要根据新的类的添加进行添加。这里又会遇到一个问题,即我们忘记重载这个方法,使得更多的错误产生。另外,各种类的各种方法都混在一起,这必然会造成类型之间的耦合性,造成混乱的局面,至少难以阅读。

如果我们使用多态,我们使用Instrument提供一个统一的接口,我们仅仅需要编写这个(基类)接口,使得任意的导出类都可以正确执行。用书中的话说:发送消息给某个对象,让该对象去断定应该做什么事。而且,tune方法根本不去考虑周围的变化,它只关注play这个方法。还有一点好处,我们现在可以随意添加新的类,而不需要更改tune方法,这使得代码具有良好的扩展性。另外,代码更简洁(至少行数更少),可读性较好,我们不会混在一大堆类与方法中,分不清道不明。

总结来看,多态通过基类接口,为我们实现了接口与实现的分离,同时消除了类型之间的耦合性。它使得可读性和可扩展性更好。书中给出了精彩的总结:

多态是一项让程序员“将改变的事物与未来的事物分离开来”的重要技术。

由于我们使用了向上转型,这使得导出类的许多新方法无法被使用,如果我们希望使用导出类的新方法,就要“向下转型”。由于向下转型可能会丢失信息(向上转型就是安全的),所以我们必须确保向下转型的正确性。实际上,Java语言对所有转型都会检查。

多态也有缺点,比如导出类不能覆盖基类中的私有方法。如果某个方法是静态的,它也不具有多态(多态是动态绑定的过程,静态域则在静态绑定阶段已经确定)。

不知道多态的概念和用法是否被解释清楚,若有不足之处,也请指正。



blog comments powered by Disqus