写作风格:力求在保证逻辑正确的前提下,用尽量通俗的语言和案例去讲解多态,尽量“嚼碎”。
概念
所谓多态,就是指程序中定义的引用变量所指向的具体类型,和通过该引用变量发出的方法调用在编程时并不确定,而是在程序运行期间才确定。换句话说,即一个引用变量倒底会指向哪个类的实例对象?该引用变量发出的方法调用到底是哪个类中实现的方法?必须在由程序运行期间才能决定。
概念理解起来很抽象,还得用具体的例子来形象的剖析。
比如,你进入一个房间,里面有三个人背对着你,从背面看是不知道这三个人到底是谁的。只有转过来看了脸才知道是谁,你一转,这是张三,再一转这是王五,再转这是李四…在这里就可以描述成:
人a = 张三
人b = 李四
人c = 王五
...
这里所表现的就是多态。张三、李四、王五都是人的子类,我们课可以通过人这一个父类就能够引用不同的子类,这就是多态。
要理解多态就必须知道什么是 “向上转型”,向上转型是在继承中学过的,这里就再说一下:在上面人的例子中,人(People)是父类、张三(ZhangSan)、李四(LiSi)、王五(WangWu)是子类。那么可以定义如下代码
People p = new ZhangSan();
怎么理解呢,这里定义了一个People类型的引用变量p,它指向的是ZhangSan对象示例。由于ZhangSan是继承于People,所以ZhangSan可以自动向上转型为People,所以p是可以指向ZhangSan的对象实例的。
这样做有一个非常大的好处,我们知道在继承中,子类是父类的扩展,它可以提供比父类更加强大的功能,如果我们定义了一个指向子类的父类类型引用,那么它除了能够使用父类的特性外,还可以使用子类的强大功能。
但是向上转型存在一定的缺憾,那就是它必定会导致一些方法和属性的丢失,从而导致我们不能够获取它们。所以 父类类型的引用可以调用父类中定义的方法和属性,对于“只存在”于子类中的方法和属性,它就望尘莫及了…
多态的实现
实现条件
Java实现多态有三个必要条件:继承、重写、向上转型。
- 继承:在多态中必须存在有继承关系的子类和父类。
- 重写:子类对父类中某些方法进行重新定义,在调用这些方法时就会调用子类的方法。
- 向上转型:在多态中需要将子类的引用赋给父类对象,只有这样该引用才能够具备技能调用父类的方法和子类的方法。
只有满足了上述三个条件,我们才能够在同一个继承结构中使用统一的逻辑实现代码处理不同的对象,从而达到执行不同的行为。
实现机制原则
对于Java而言,它多态的实现机制遵循一个原则
(非常重要!!!以下简称 “实现机制原则”):当超类对象的引用变量引用了子类对象时,被引用对象的类型决定了调用谁的成员方法,而不是引用变量的类型决定的。但是同时这个被调用的方法必须是在超类中定义过了,也就是说,被子类重写覆盖的方法。
这个原则很重要,一直会贯穿以下全文!!!
实现形式
在Java中有两种形式可以实现多态:继承和接口。
基于继承实现的多态
对于引用子类的父类类型,在处理该引用时,它适用于继承该父类的所有子类,子类对象的不同,对方法的实现也就不同,执行相同动作产生的行为也就不同。
继承链中方法调用原则
注意:当子类重写父类的方法被调用时,只有对象继承链中的最末端的方法才会被调用
基于接口实现的多态
在接口的多态中,指向接口的引用必须是指定这实现了该接口的一个类的实例程序,在运行时,根据对象引用的实际类型来执行对应的方法。
继承都是单继承,只能为一组相关的类提供一致的服务接口。但是接口可以是多继承多实现,它能够利用一组相关或者不相关的接口进行组合与扩充,能够对外提供一致的服务接口。所以它相对于继承来说有更好的灵活性。
经典案例
在阐述了一系列的概念之后,需要执行实例来证明这写概念,这样就会理解得更加透彻。毕竟 实践是检验真理的唯一标准。
代码如下:
public class A {
public String say(A a){
return "A and A";
}
public String say(D d){
return "A and D";
}
}
public class B extends A{
@Override
public String say(A a) {
return "B and A";
}
public String say(B b){
return "B and B";
}
}
public class C extends B{}
public class D extends B{}
public class MainTest {
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("1--:" + a1.say(b));
System.out.println("2--:" + a1.say(c));
System.out.println("3--:" + a1.say(d));
System.out.println("4--:" + a2.say(b));
System.out.println("5--:" + a2.say(c));
System.out.println("6--:" + a2.say(d));
System.out.println("7--:" + b.say(b));
System.out.println("8--:" + b.say(c));
System.out.println("9--:" + b.say(d));
}
}
运行结果:
1--:A and A
2--:A and A
3--:A and D
4--:B and A
5--:B and A
6--:A and D
7--:B and B
8--:B and B
9--:A and D
从上面的程序可以看出A、B、C、D存在如下关系:
继承链方法调用优先级
再多说一句话:其实在继承链中对象方法的调用存在一个优先级:
(非常重要!!!以下简称 “调用优先级”)this.say(Obj)、super.say(Obj)、this.say((super)Obj)、super.say((super)Obj)。
然后带着这句话以及上面所提到的 “实现机制原则”和“调用优先级”来逐个剖析这几个打印语句打印的结果。在看下面剖析之前可以自己在心里看看自己的想的打印是否和结果一致。最好是自己思考一遍,然后带着疑问来看下面的剖析。
分析
分析结果一
a1.say(b) 的结果是"A and A",已知a1是A类型对象的引用变量,a1所指向的对象示例是A类型对象,因为new A();。
- 第一步:将优先级套进来,第一级:this.say(Obj),this就代表了A,根据实现机制原则,在A类中去寻找say(b)这个方法,结果没有找到。
- 第二步:那么紧接着看第二优先级:super.say(Obj),super代表A类的超类,由于A没有超类(Object除外)
- 第三步:所以跳到第三优先级:this.say((super)Obj),this就代表了A,(super)Obj就是(super)b,b的超类是A,于是就演变成了A.say(A),根据实现机制原则,去A类中去寻找有没有该方法,结果是有。
- 所以结果是"A and A"
分析结果二
a1.say©的结果是"A and A",已知a1是A类型对象的引用变量,a1所指向的对象示例是A类型对象,因为new A();。
- 第一二步的分析过程和结果一的一二步一致。
- 第三步:差别在于c的超类是B和A,先将c向上转型成B,变成
A.say(B),然后根据实现机制原则,去A类中去寻找有没有say(B)这个方法,结果是没有,然后再将B向上转型成A,就演变成了A.say(A),根据实现机制原则,现在去A类中去寻找有没有该方法,结果是有。
所以结果是"A and A"
分析结果三
a1.say(d) 的结果是"A and D",已知 a1是A类型对象的引用变量,a1所指向的对象示例是A类型对象,因为new A();。
- 这个结果的分析过程很简单,根据实现机制原则,去A类中寻找say(d),直接找到了这个方法.
- 所以结果就是“A and D”
前三个结果是只涉及到了继承中方法调用的优先级,没有涉及到多态。下面开始就会涉及到方法调用的优先级和多态。
分析结果四(迷魂阵)
a2.say(b)的结果是“B and A”,看到这里就可能很迷惑了,为什么不是“B and B”呢?
已知 a2是A类型对象的引用变量,a2所指向的对象示例是B类型对象,因为new B();。
- 第一步:将优先级套进来,第一级:this.say(Obj),this就代表了A,根据实现机制原则(再次提醒以下:被引用对象的类型决定了调用谁的成员方法),那么就在B类中去寻找say(b)这个方法,结果是找到了!!!
那么为什么没有直接执行B类中的say(B)方法以致输出结果为“B and B”呢?那是因为我们忽略实现机制原则中的后面那句话
“但是同时这个被调用的方法必须是在超类中定义过了,也就是说,被子类重写覆盖的方法。”
换句话说,say(B)这个方法必须已经在B类的超类也就是A类中定义过了,再换句话说,B类中的这个say(B)方法不是普通的成员方法,它是重写的父类的方法。那么根据现在的情况来说,say(B)方法在A类中存在吗?根本就不存在!所以这句话在这里不适用。那么难道是这句话错了?非也!其实这句话还隐含着后面这句话:“它仍然要按照继承链中调用方法的优先级来确认”。那么按照调用优先级接着分析。 - 第二步:接着看第二优先级:super.say(Obj),super代表A类的超类,由于A没有超类(Object除外)
- 第三步:所以跳到第三优先级:this.say((super)Obj),this就代表了A,(super)Obj就是(super)b,b的超类是A,于是就演变成了A.say(A),虽然这里演变成A.say(A),但是并不会去调用A类中的say(A)方法,仍旧根据实现机制原则,去B类中去寻找有没有该方法,结果是有。由于B重写了该方法,所以才会调用B类中的方法所以这里会调用B的成员方法say(A)。
- 所以结果是"B and A"
后面五个结果的分析过程基本和前四个结果的分析过程大同小异,就不逐一分析了。自己也试着分析一下,看看到底掌握了没有。
总结
所以多态机制遵循的原则可以概括为:
当超类对象的引用变量引用了子类对象时,被引用对象的类型决定了调用谁的成员方法,而不是引用变量的类型决定的。但是同时这个被调用的方法必须是在超类中定义过了,也就是说,这个被调用的方法是被子类重写覆盖过的方法。然后它仍然要根据继承链中方法调用的优先级来确认调用的方法,该优先级为:this.say(Obj)、super.say(Obj)、this.say((super)Obj)、super.say((super)Obj)。
技 术 无 他, 唯 有 熟 尔。
知 其 然, 也 知 其 所 以 然。
踏 实 一 些, 不 要 着 急, 你 想 要 的 岁 月 都 会 给 你。