多态是Java三大特性之一(继承、封装、多态),多态的存在依赖于继承的存在,也就是说,只有当程序中出现父子关系,才能体现出多态。
多态的定义
为了理解多态的定义,我们先来看一个生活中的例子:手机人人都有,手机上的音量键在默认情况下调节的是手机扬声器音量的大小,在读书app界面音量键却可以翻页,有的手机音量键可以快速拍照、截屏。同样的音量键,同样的触发方式,在不同情况下功能不同。
多态的定义就是允许同一函数调用根据调用对象的不同而采取多种不同的响应方式。
多态存在的条件
- 存在继承
- 子类重写父类方法
- 父类引用指向子类对象(相当于向上转型)
一个小例子
class A{ //父类
public String show(D obj){
return ("A and D");
}
public String show(A obj){
return ("A and A");
}
}
class B extends A{ //子类
public String show(B obj){
return ("B and B");
}
@override
public String show(A obj){
return ("B and A");
}
}
class C extends B{...} //子类
class D extends B{...} //子类
public class Test{
public static void main(String args[]){
A a1 = new A();
A a2 = new B();
B b = new B();
C c = new C();
D d = new D();
System.out.println(a1.show(b)); //A and A
System.out.println(a1.show(c)); //A and A
System.out.println(a1.show(d)); //A and D
System.out.println(a2.show(b)); //B and A
System.out.println(a2.show(c)); //B and A
System.out.println(a2.show(d)); //A and D
System.out.println(b.show(b)); //B and B
System.out.println(b.show(c)); //B and B
System.out.println(b.show(d)); //A and D
}
}
方法调用过程
假设要调用x.f(param)
,x声明为类C
的一个对象,详细调用过程如下:
- 编译器查看对象的声明类型和方法名。例如要调用
x.f(param)
,编译器会找出类C中所有名为f的方法以及类C的超类中所有访问属性为public和方法名为f的方法。至此,编译器获得所有可能调用的方法。 - 接下来,编译器会查看调用方法时提供的参数类型。如果在所有名为f的方法中存在一个与提供参数类型完全匹配的方法,就调用该方法。这个过程被称为重载解析。这个过程允许类型转换,所以可能十分复杂。如果编译器在类型转换后没有找到一个匹配的方法或者存在多个匹配的方法,则报告一个错误。(注:如果在子类中定义了一个与超类相同签名的方法,则会用子类的方法覆盖超类中相同签名的方法)
- 如果是
private
方法、static
方法、final
方法或者构造器,编译器就能很快知道调用哪个方法,这种方式称为静态绑定。与此相对应的是,调用方法依赖于隐式参数的实际类型,并且在运行时实现动态绑定。 - 当程序运行时,虚拟机一定调用与x所引用对象的实际类型最合适的那个类的方法。
多态成员的特点
- 多态成员变量,编译运行都看左边。
- 多态成员方法,编译看左边(即引用类型),运行看右边(即实际指向对象)。
根据多态成员的特点,对上述小例子做一个解析:
A a1 = new A()
,a1引用类型为类A,实际指向的对象也是类A的对象,在编译过程中,a1的方法表为:
a1.show(D obj);
a1.show(A obj);
于是在调用方法的时候,a1.show(b)
和a1.show(c)
调用方法表中的第二个方法(涉及到参数类型的转换),a1.show(d)
调用方法表中的第一个方法。
A a2 = new B()
,a2引用类型为类A,实际指向对象是类B的对象,在编译过程中,a2的方法表为:(结合编译看左边,即编译看引用类型)
a2.show(D obj);
a2.show(A obj); //运行时会被子类重写的方法覆盖
在调用方法时,a2.show(b)
和a2.show(c)
调用方法表中的第二个方法(涉及到参数类型的转换),但是在运行过程中看声明的右边,即看实际指向的对象,类B重写了类A中的方法,所以方法表中的第二个方法被覆盖为子类中重写的方法,实际调用类B中的第二个方法,a2.show(d)
调用方法表中的第一个方法。
B b = new B()
,引用类型为类B,实际指向对象为类B的对象,在编译过程中,b的方法表为:
b.show(B obj);
b.show(A obj); //重写超类方法
b.show(D obj); //继承超类方法
于是在调用方法的时候,b.show(b)
和b.show(c)
调用方法表中的第一个方法(后一个调用涉及到参数类型的转换),b.show(d)
调用方法表中的第三个方法(实际上是调用继承自超类的方法)。