java中的多态
多态是面向对象三大特性之一(另外两个叫封装和继承),也是三兄弟中最不好理解的一个。网上流行的说法是分两类多态:编译时和运行时。有的人把重载、泛型算到编译时多态头上,有的人不同意,我站在后面这一队。为啥?因为java语言的经典著作《Thinking in JAVA》里说:多态通过分离做什么和怎么做,从另一个角度将接口和实现分离开来。这里的接口泛指基类、接口类,因此多态应该基于类的继承或者接口的实现而言,而重载、泛型并未涉及。
先说继承,看例子:
父类:
abstract class Death { abstract void printDeathWay(); }
子类:
public class AccidentDeath extends Death { @Override public void printDeathWay() { System.out.println("I'm killed in an accident."); } }
public class DisasterDeath extends Death { @Override public void printDeathWay() { System.out.println("I'm killed by a disaster."); } public static void main(String[] args) { Death death = new AccidentDeath(); death.printDeathWay(); // 多态:Death -> AccidentDeath death = new DisasterDeath(); // 多态:Death -> DisasterDeath death.printDeathWay(); } }
执行结果:
I'm killed in an accident. I'm killed by a disaster.
我们看到父类Death执行了printDeathWay方法有两个不同的子类结果,因为子类分别重写了该方法,这就是多态。同样的,如果把Death的abstract class改为interface,子类extands改为implememts,执行结果仍然不变。接口的行为由实现类分别去处理。这里抽象方法printDeathWay调用的时候用到了动态绑定。
啥是动态绑定?将一个方法调用同一个方法主体关联起来被称为绑定。如果在程序执行之前就绑定了,就叫前期绑定或静态绑定,比如static方法、私有方法或者构造器,编译器很明确该调用哪个方法。比如上面的main方法就是一个static方法,编译器在执行main方法前就知道去调用该main方法。但如果是在运行时根据对象的类型进行绑定,那么就叫后期绑定,也叫做动态绑定或运行时绑定。动态绑定是java的默认方式(除了上面静态绑定之外的方法调用),正式因为天赋此种异凛,才导致了多态特性的诞生。那么动态绑定怎么做到的呢?JVM为每个方法签名和实际调用方法建立映射关系,每次调用方法时JVM都去映射关系表里查找。比如上面调用death.printDeathWay()时,JVM先找到death的具体类型(比如是AccidentDeath),然后查找该类的方法映射表(printDeathWay() -> AccidentDeath.printDeathWay()),最后调用AccidentDeath.printDeathWay()。
最后再说下重载的情况:
public void printSomething(String msg) { System.out.println(msg); } public void printSomething(int count) { System.out.println(count); }
上面的两个方法名称相同,但参数类型不同,乍一看也有点多态的意思,同一个方法调用支持不同的类型,但我们要明白,重载是横向的,而多态是纵向的。泛型也是同样的道理,它们都是编译器能识别的(重载如果方法签名一样则编译报错,泛型传入类型不一致也编译报错)。只有纵向的继承(包括接口的实现)才是多态的特征,这就是为什么我认为没有编译时多态这一说法的根源。