【面向对象特征系列之“多态”】

面向对象特征当中最让初学者头疼的就算是“多态”了🤦‍♂️。“封装”、“继承”的概念大家在生活中还多多少少接触过,还能够做一定层度上的类推,但“多态”这个非生活用词就显得比较陌生了。
多态”的用途和表现形式又非常多,总是在程序设计与开发中出现,弄得的大家不知所措。这儿也是多态,那儿也是多态,那么多态到底是个啥呢?😭

什么是多态

多态的概念:

多态(Polymorphism)这个概念最早来自于生物学,表示的是同一物种在同一种群中存在两种或多种明显不同的表型。比如:在南美种群中存在两种颜色的美洲虎:浅黄色的和黑色的。

而在面向对象编程思想中,这个概念表达的是具有共性的类型,在执行相同的行为时,会体现出不同的实现方式。我们可以简称为:相同的行为,不同的实现。 比如:同样看到对面过来一个美女,男人和女人的想法是不一样的。

多态在面向对象中的地位

一说到面向对象特征,大家都能马上想到“封装”、“继承”、“多态”。
1、“封装”是面向对象的基础,它让我们能够把现实环境的复杂内容进行归类,让编程无论在理解度上还是在语法上进行更好的表达;
2、“继承”表达的是“重复”,是复用性的体现,能够让我们通过找到类型的共性进行更进一步的提取和划分;
3、那么,“多态”则是多样性、可扩展性的体现。面对丰富的和可能不断变化的问题域,让我们的程序能够有更大的容纳性去模拟和适应这些变化。它表达的是“变化”,这就是为什么“多态”能够跻身面向对象三大特征的原因!


注意:由于“多态”的概念是---相同的行为,不同的实现;所以,对我们而言只有方法才有“多态”性的体现,这点别搞错了~~~

多态的分类

多态分为两种形式:静态多态和动态多态。
这里的静态与关键字static并没有什么关系。动静之分主要是表现在程序是在运行期还是编译期,通过被绑定的对象类型来决定到底执行的是哪个方法。
静态多态是在编译期就确定了对象以及对象行为的绑定关系,所以运行起来以后就固定为编译期确定的效果;
而动态多态是在编译期未知绑定关系,运行进行以后才进行绑定,所以具有更大的灵活性;
很明显,动态多态具有更大的灵活性和可扩展性,所以它才是我们使用率更高的多态方式,也是这篇博客要讲述的重点。

静态多态

诚如上面所言,静态多态是在编译期间就可以确定要执行的是何种类型的对象以及该对象的何种行为,运行期不会有改变的情况。所以 方法重载单独使用方法重写 都是它的具体表现形式。

方法的重载

方法重载:
在一个类当中,具有多个同名方法,参数列表不同(包括:参数个数、参数类型、参数顺序的不同),从而各有各的实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Student{
public void study(){
System.out.println("自习");
}
public void study(String subject){
System.out.println("完成当天课程:" + subject + "的课后作业。");
}
}
public class Teacher{
private Student stu;
public void teach(){
System.out.println("现在布置任务:");
//stu.study();
stu.study("数学");
}
}

讲解:
1、Student(学生)类当中拥有两个同名方法study(学习),一个无参、一个带参,这是标准的重载语法。
2、两个study方法之所以同名,是因为它们都是学生类的“学习”行为,但各有各的实现,所以这符合“相同的行为,不同的实现”,这是一个“多态”的设计。
3、Teacher(老师)类中绑定了一个Student对象stu,在它的teach方法中调用了stu对象的study行为;如果调用代码是不传参的,那么运行效果一定是“自习”的效果;如果传的是字符串参数,那么运行效果一定是“完成课后作业”的效果。在这里体现了:运行的最终效果,其实在编译Teacher类的时候就被确定了。要想改变运行效果,那么只有改写teach方法中的调用代码。这就是所谓的“静态多态”。

方法的重写

方法重写:
在继承关系中,不同的子类都拥有继承于父类的某个共有方法,但是各有各的实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public abstract class Gun{
public abstract void fire();
}
public class Rifle extends Gun{
public void fire(){
System.out.println("砰~~~");
}
}
public class MachineGun extends Gun{
public void fire(){
System.out.println("哒哒哒哒哒");
}
}
public class Soldier{
//private Rifle gun;//绑定步枪对象
private MachineGun gun;//绑定机枪对象
public void fight(){
System.out.println("射击!");
this.gun.fire();
}
}

讲解:
1、Gun(枪)类有一个方法叫做fire(开火)。Rifle(步枪)和MachineGun(机枪)都是Gun的子类,各自重新实现自己fire方法的特殊实现。这是标准的重写语法。
2、步枪,机枪都是枪,都拥有开火行为,但各有各的实现,所以这符合“相同的行为,不同的实现”,这是一个“多态”的设计。
3、Soldier(士兵)类中绑定了一个MachineGun对象gun,在它的fight方法中调用了该对象的fire方法。这时运行fight方法的效果一定是机枪的“哒哒哒哒”效果;如果要换成步枪的效果,那么只有把绑定的gun对象类型从MachineGun换成Rifle。在这里同样体现了:运行的最终效果,其实在编译Soldier类的时候就被确定了。要想改变运行效果,那么只有改写gun对象的类型声明代码。这也是“静态多态”。

动态多态

相对于静态多态,动态多态是什么效果呢?闲话少叙,先让我们看看代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public abstract class Gun{
public abstract void fire();
}
public class Rifle extends Gun{
public void fire(){
System.out.println("砰~~~");
}
}
public class MachineGun extends Gun{
public void fire(){
System.out.println("哒哒哒哒哒");
}
}
public class Soldier{
private Gun gun;//绑定枪对象
public void fight(){
System.out.println("射击!");
this.gun.fire();
}
}

大家很容易就发现,这个动态多态的例子其实就是改动了一下上面静态多态中的第二个例子。差异在哪儿呢?为什么这个就“动态”了,之前那个就“静态”了呢?
其实差异就只有这一句代码而已:

1
private Gun gun;

在这个动态绑定的例子当中,Soldier(士兵)类中绑定的不再是某一个Gun(枪)的子类对象,而是父类Gun(枪)的对象。也许有同学会瞪大眼睛疑问:“Gun不是抽象类吗?不能产生对象呀?”
其实,Gun是不是抽象类根本无所谓。因为在这里,我们压根儿也没有想过要让gun指向一个Gun对象,我们需要的就是它既可以指向Rifle(步枪)对象,也可以指向MachineGun(机枪)对象。无论以后运行起来绑定谁,都不需要修改Soldier(士兵)的代码!就算将来还有可能增加其它的枪(手枪,狙击枪,火药枪),只要它是枪,我们的Soldier中已有的绑定代码就可以直接使用,不用修改!!

父类的引用指向子类的对象

是的,只要它是枪!在这里,我们描述了在继承关系(is a)中出现的场景。在Java中除了本类的引用指向本类的对象,还可以让父类的引用指向子类的对象

1
2
Gun g1 = new Rifle();//正确
Gun g2 = new MachineGun);//正确

场景上,“枪“当然可以是一把步枪,也可以是一挺机枪。“枪”类型可以指代任何一种枪对象,这与“步枪”类型只能指代步枪对象,“机枪”类型只能指代机枪对象是不同的。当然前提条件是要有继承关系,这个很生活吧😊。
内存中,由于Java的对象内存模型采用的是“内存叠加的方式”,即:在子类对象产生的时候,会先调用父类的构造方法产生一个父类对象部分,然后再调用子类自己的构造方法,在该父类对象部分下面叠加上子类特有部分,从而形成一个完整的子类对象!所以,我们可以认为每一个子类对象中都包含了一个完整的父类对象部分。所以,当用父类引用指向这个子类对象的时候,JVM会发现这个对象确实有父类对象所有的内容,在编译上是没有问题的。

所以,当我们绑定一个父类引用的时候,它既有可能指向父类的对象,也有可能是指向该父类的某个子类的对象。它是可变的,具体指向谁不是由编码期的声明决定的,而是可以通过在运行时传入不同的对象,从而形成所谓的“动态绑定”

动态绑定与重写

当父类引用指向子类对象以后,在使用这个引用调用方法的时候会发生什么呢?其实无非就三种情况

  1. 使用父类引用调用父类定义的方法;
  2. 使用父类引用调用子类定义的方法;
  3. 使用父类引用调用父类定义,但被子类重写了的方法。

第一种情况:其本质就是本类的引用调用本类的方法,所以也就没有任何编译或运行的问题;在知识点上,你只需要通过上面的图例3认清楚一点,子类对象中包含有来自于父类对象部分,那么理解起来就很简单了!

第二种情况:你在调用的时候会发现根本调用不到,编译就会马上报错。其道理也很简单:虽然对象是子类对象,其中当然包含有子类自己定义的内容,但是由于我们的引用是父类类型(即我们是站在父类的层面去看待它),当然就看不到这些子类定义的内容了。如果想访问,那么必须进行“向下转型”。参看:向上转型,向下转型,还在头疼?

第三种情况:这是我们在本博中所需要的“动态多态”的知识点。由于这个方法是重写方法,首先说明它的定义就是在父类当中,所以利用父类的引用当然可以看到有这个方法,保证能够使用引用调用得到;其次该方法被子类对象重写了,而且不同的子类可以各有各的实现,那么具体的运行效果,就要依赖于这个父类引用运行起来以后,到底是指向哪种子类的对象了(指向谁,就执行谁的重写后效果)。
完整代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public abstract class Gun{
public abstract void fire();
}
public class Rifle extends Gun{
public void fire(){
System.out.println("砰~~~");
}
}
public class MachineGun extends Gun{
public void fire(){
System.out.println("哒哒哒哒哒");
}
}
public class Soldier{
private Gun gun;//绑定枪对象
public void fight(){
System.out.println("射击!");
this.gun.fire();
}
//省略get/set方法,请自行添加
}
public class TestMain{
public static void main(String[] args){
Soldier rambo = new Soldier();
rambo.setGun(new Rifle());
//rambo.setGun(new MachineGun());
rambo.fight();
}
}

通过测试这个代码,我们可以看到,如果给rambo(兰博)对象传入Rifle(步枪)对象,那么他战斗的时候就是步枪的效果;如果传入MachineGun(机枪)对象,那么rambo(兰博)对象战斗就是机枪的效果。换步枪、换机枪,但是Soldier和Gun的绑定代码不变,就算以后有别的子类枪,Soldier的代码也无需修改,这就是我们需要的“动态多态”效果。

总结

首先,我们来总结一下知识点:
这里,我就不再写出答案了,请各位阅读者自行归纳描述。如果有需要,可以再返回到上面仔细阅读。

  1. 多态的概念是什么?
  2. 什么是静态多态?什么是动态多态?
  3. 动态多态是依赖什么技术实现的?
  4. 动态多态的好处是什么?

其次,以后我们可以扩展到哪些知识点:

  1. 接口的引用也可以指向实现类的对象;
  2. 反射实现动态产生对象;
  3. Spring完成IOC注入;
  4. 桥梁模式、装饰器模式、策略模式等常见设计模式;
  5. 聚合组合原则、依赖倒转原则等常见设计原则。