多态通过分离做什么和怎么做,从另一个角度将接口和实现分离开来。多态不但能够改善代码的组织结构和可读性,还能创建可扩展的程序,无论在项目最初还是添加新功能的时候都是可“生长”的程序。简单的来说多态就是将派生类的引用赋给基类,并通过基类的引用调用派生类的方法(前提派生类重写了基类的方法)。多态也称动作绑定,后期绑定或运行时绑定。对于不了解方法重写和向上转型的人们来说是很难理解多态的(下面将会介绍)。多态的作用是消除类型之间的耦合关系。
方法重写(Override)和向上转型
方法重写很容易理解,即两个方法的方法名、参数列表必须完全一致(一个方法在基类中,一个方法在父类中) ,派生类方法的返回类型必须是基类方法返回的类型,或是基类方法返回类型的派生类(协变返回类型)。例如:
class Grain {//农作物 public String toString() { return "Grain"; }//重写了object类的toString方法 } class Wheat extends Grain {//小麦 public String toString() { return "Wheat"; } } class Mill {//加工厂 Grain process() { return new Grain(); } } class WheatMill extends Mill {//加工小麦 Wheat process() { return new Wheat(); }//这里返回了基类返回类型的派生类(协变返回类型) } public class CovariantReturn { public static void main(String[] args) { Mill m = new Mill(); Grain g = m.process(); System.out.println(g); m = new WheatMill(); g = m.process(); System.out.println(g); } }
对了这里要注意一点private,fianl和static方法不能被重写。
对象既可以作为它自己本身的类型使用也可以作为它的基类类型使用。而这种把某个对象的引用视其为基类类型的引用称为向上转型。简单的说就是将派生类的对象引用赋给基类,可能我这么理解有些粗。在上一个例子中m = new WheatMill();
就是向上转型。在看一个乐器类的例子:
乐器要弹奏音符,所以我们单独创建一个乐符类Note。
public enum Note {//乐符类 MIDDLE_C, C_SHARP, B_FLAT; // Etc. } class Instrument {//基类 public void play(Note n) { print("Instrument.play()"); } } public class Wind extends Instrument { public void play(Note n) { System.out.println("Wind.play() " + n); } } public class Music { public static void tune(Instrument i) { i.play(Note.MIDDLE_C); } public static void main(String[] args) { Wind flute = new Wind(); tune(flute); // 向上转型 } }
Mumsic.tune()方法接受一个Instrument引用,同事也接受Instrument子类的引用。在main()方法中,当向trune方法传递Wind引用时,就会出现向上转型,不需要任何类型转换,这是编译器帮我们完成的。 这样做是也许的,因为Wind继承自Instrument类,所以Instrument接口包含在Wind中。从Wind向上转型为instrument可能接口会"缩小",但是不会比instrument接口更窄。因为很有可能派生类不但重写了基类所有方法,而且还有自己的方法。这就要设计到继承的两种关系is-a和is-like-a,这里不说了。
有些人可能会感到奇怪为什么不让tune方法直接接受Wind的引用作为自己的参数呢?这样会更为直接。假设按照这种推理,现在我们添加其他的的乐器类,比如添加Stringed(弦乐)和Brass(管乐):代码如下:
class Stringed extends Instrument { public void play(Note n) { print("Stringed.play() " + n); } } class Brass extends Instrument { public void play(Note n) { print("Brass.play() " + n); } } public class Music2 { public static void tune(Wind i) { i.play(Note.MIDDLE_C); } public static void tune(Stringed i) { i.play(Note.MIDDLE_C); } public static void tune(Brass i) { i.play(Note.MIDDLE_C); } public static void main(String[] args) { Wind flute = new Wind(); Stringed violin = new Stringed(); Brass frenchHorn = new Brass(); tune(flute); // No upcasting tune(violin); tune(frenchHorn); } }
这样做也是行的通的,但是我要为每添加一种新的乐器添加一个对于的tune方法,如果要有上百种乐器,那你也添加上百个tune方法。显然这种写法比较垃圾,只有初学者才可能这样写。如果我们将接受参数为基类,而不是派生类。那么我们只需编写一个tune方法。将基类作为接口,直接与基类打交道。忽略派生类。这也正是多态所也许得。可以看到多态和继承之间的密切关系。
有人可能会发现在上面的例子中tune方法接受一个Instrument引用,当我们传入Wind对象时,编译器怎么知道Instrument指向的是Wind对象,而不是Brass或String对象呢?这是因为java中方法绑定的机制,即将一个方法调用同一个方法体关联起来被称为绑定。若在程序执行之前进行绑定称为前期绑定。在程序运行时进行绑定称为后期绑定(运行时绑定,动态绑定),即多态。前面我们所疑惑的问题其实就是运用了后期绑定。这里知道就可以,具体怎么做的那是编译器的事。
从下边的例子我们可以这样理解多态,Instrumen基类就像一个乐器团,这个乐器团中有很多乐器(像Stringed和Brass ),当对它们下达同一命令时(要调用哪个方法,比如演奏命令,这里就是调用paly()),它们就会按照自己的方式(play方法中的具体实现)去执行命令(调用play方法)。那我弦乐就是拉,管乐就是吹。
class Stringed extends Instrument { public void play(Note n) { print("Stringed.play() " + n); } } class Brass extends Instrument { public void play(Note n) { print("Brass.play() " + n); } } public class Music2 { public static void tune(Instrument i) { i.play(Note.MIDDLE_C); } public static void main(String[] args) { Wind flute = new Wind(); Stringed violin = new Stringed(); Brass frenchHorn = new Brass(); tune(flute); // No upcasting tune(violin); tune(frenchHorn); } }