多态是面向对象程序设计三大特征之一,所谓多态就是指程序中定义的引用变量所指向的具体类型和通过该引用变量发出的方法调用在编程时并不确定,而是在程序运行期间才确定,即一个引用变量倒底会指向哪个类的实例对象,该引用变量发出的方法调用到底是哪个类中实现的方法,必须在由程序运行期间才能决定。因为在程序运行时才确定具体的类,这样,不用修改源程序代码,就可以让引用变量绑定到各种不同的类实现上,从而导致该引用调用的具体方法随之改变,即不修改程序代码就可以改变程序运行时所绑定的具体代码,让程序可以选择多个运行状态,这就是多态性。
1、向上转型
要理解多态就必须理解向上转型,考虑以下例子,shape1和shape2是Shape类型,分别赋值的为Shape的两个子类Circle和Rectangle的实例,这说明Circle和Rectangle是可以转型为Shape类型,所以shape可以指向Circle和Rectangle的实例,这就是向上转型。
1 public class Shape{ 2 void draw(){} 3 } 4 5 public class Rectangle extends Shape { 6 void draw() { 7 System.out.println("draw Rectange"); 8 } 9 }10 11 public class Circle extends Shape {12 void draw() {13 System.out.println("draw circle");14 }15 }16 17 public class Test {18 public static void main(String []args) {19 Shape shape1 = new Circle();20 Shape shape2 = new Rectangle();21 22 shape1.draw();23 shape2.draw();24 }25 } out: draw circle draw Rectange
2、动态绑定
上面的例子中,shape1和shape2调用draw,可以从输出看出,实际调用的是子类的draw,而不是shape1和shape2本身的类型Shape的draw,这是为什么呢?这里必须要知道函数绑定的概念,将一个函数调用与函数主题连接在一起就叫做函数绑定(binding),若在程序运行之前执行绑定(编译阶段和链接阶段),叫做早期绑定,比如熟悉的C语言;若在程序运行时执行绑定,就叫做动态绑定(后期绑定),这样即可以在运行时确定对象的类型,并正确调用相应的函数,不同的语言对于动态绑定的实现时不一样的,java中的绑定采用的都是动态绑定(final函数除外,final方法不能被继承)。
从上面的例子可以看出,虽然shape1和shape2是Shape类型,但是在运行过程中确可以准确的知道shape1和shape2指向实例的实际类型,并且正确的调用了相应的draw函数。
3、私有方法的“多态”
考虑下面的例子,Son继承了Father的私有函数,但是最后的结果中却没有按照我们“意想”的那样调用Son的f(),这位因为private方法会被默认为final方法,final是不能被继承的,而且private方法对于继承类是不可见的,在这种情况下,Son的f()方式其实是一个全新的方法,所以此处调用的任然是Father的f()方法。
注意:只有非private方法是可以继承的,同时需要注意覆盖private方法的情况,这种情况下编译器不会报错,但是也不会像我们“意想”的那样执行,所以,在导出类中,对于基类的private方法,最好采用不同的名字
1 //Father.java 2 public class Father { 3 private void f() { 4 System.out.println("this is Father"); 5 } 6 public static void main(String []args) { 7 Father father = new Son(); 8 father.f(); 9 }10 }11 12 //Son.java13 public class Son extends Father {14 public void f() {15 System.out.println("this is Son");16 }17 }18 19 out:20 this is Father
4、静态方法
静态方法不具有多态性,静态方法是与类,而不是与单个的对象相关联的。
不能在继承中更改静态属性,即父类中的方法是static,而子类中同名的方法确实非static(或者父类是非static方法,子类同名的方法是static),此时编译器会直接报错
5、域
只有普通的方法调用时多态,域访问操作都由编译器解析,所以域是不具有多态性,考虑如下例子,直接访问域时,father.field的输出为0,son.fiedl的输出为1,说明域并没有多态性,后面运行f()方法时,输出皆为1,说明普通方法的调用的多态。
在域的继承时,Father.field和Son.field分配了不同的存储空间,这样,实际上Son中包含了两个field域,它自己的和它从Father继承来的,在Son的引用中的field默认时自己的field,而不是从父类继承的field,而要访问从父类继承的field,必须显式使用super.field,如下方法prtSuperField()中所示。
在实际的应用中,对于应尽量定义成private,同时在继承中应当避免在导出类中使用与父类的域相同的名字。
//Father.javapublic class Father { public int field = 0; public void f() { System.out.println("this is Father, field = " + field); } public static void main(String []args) { Father father = new Son(); Son son = new Son(); System.out.println("father.field = " + father.field); System.out.println("son.fied = " + son.field); father.f(); son.f(); son.prtSuperField(); }}//Son.javapublic class Son extends Father { public int field = 1; public void f() { System.out.println("this is Son, field = " + field); } public void prtSuperField() { System.out.println("this is son, fater.field = " + super.field); }}//out:father.field = 0son.fied = 1this is Son, field = 1this is Son, field = 1this is son, fater.field = 0
6、构造器的“多态”
构造器不同于其他方法,构造器实际上是隐式的static,所以构造器实际上是不具有多态。
考虑一种特殊的场景,在构造器中调用正在构造的对象的某个动态绑定方法,即父类的构造器调用了方法f(),同时子类重写了方法f(),根据构造器的调用顺序,子类初始化的时候,会优先调用父类的构造器,然而此时出现了特殊的情况,父类的构造器中调用了方法f(),根据多态性,此时我们认为应该调用子类的f()方法,但是,此时子类对象并没有完成初始化,即子类的方法f()在子类被构造之前被调用了,这可能导致一些难以预料到的错误,如下例子:
//Father.javapublic class Father { public Father() { System.out.println("this is father before f()"); f(); System.out.println("this is father after f()"); } public void f() { System.out.println("this is Father, in f()"); } public static void main(String []args) { Father father = new Son(); }}//Son.javapublic class Son extends Father { public int field = 1; public void f() { System.out.println("this is Son, in f()"); }}//outthis is father before f()this is Son, in f()this is father after f()
7、协变返回类型
所谓的协变返回类型是在java se5中加入的新特性,即导出类的重写的方法的返回值可以是基类对应方法的返回值的某种导出类,说起来比较绕,举个简单的例子,前面我们使用Shape类和Circle类,Circle继承于Shape,假设在Father类中有一个方法f(),返回值为Shape,那么在Son类中重写方法f()时,返回值可以为Circle(以及其他Shape的导出类)。
8、方法重载
既然子类可以覆盖父类中的方法,那么重载的情况呢,考虑如下例子,子类“重写”了方法f(),但是入参不一样,但是输出结果,却没有调用子类的方法,这是怎么回事呢?在java中,方法重载虽然方法名是一样,但是实际是不同的方法,所以在Son中的f(char)其实和父类的f(int)是不同的方法,自然不会发生动态绑定,所以Father得引用调用的永远是自己的f(int)方法
//Father.javapublic class Father { public void f(int c) { System.out.println("this is Father" + c); } public static void main(String []args) { Father father = new Son(); father.f('a'); father.f(1234); }}//Son.javapublic class Son extends Father { public int field = 1; public void f(char x) { System.out.println("this is Son" + x); }}//outthis is Father97this is Father1234
在考虑以下例子,当引用换成了Son时,根据入参的类型分别调用了父类的f(char)和自己的f(int),这说明在Son中存在两个f()方法,继承父类的和自己新写的,根据入参的不同,调用相应的方法
//Father.javapublic class Father { public void f(int c) { System.out.println("this is Father " + c); } public static void main(String []args) { Son son = new Son(); son.f('a'); son.f(1234); }}//Sonpublic class Son extends Father { public int field = 1; public void f(char x) { System.out.println("this is Son " + x); }}//outthis is Son athis is Father 1234