面向对象语言的三大特性:封装、继承、多态,而在这三种特性中,多态又是那个极具意义的一个,从某个方面来说,一个OOP语言最核心的特征就是多态,封装继承在很多方面都是为了实现多态而出现的。
而多态又可以分为两种:
1、编译时多态(静态多态):在编译的时候就知道要调用的方法,如重载
2、运行时多态(动态多态,又称动态绑定):在运行的时候才知道要调用哪个方法
重载是在我们单独拎出来的一个定义,而我们常说的多态一般指运行时的多态,也就是说我们要等最后运行的时候才会确定要调用的方法,这与Java虚拟机的运行机制有关,所以多态方法又称延迟方法。
多态的实现方法有两种:
1、子类继承父类:子类继承父类并重写了父类的方法
2、实现接口方法:实现接口的类实现了接口的方法
其实这两种方法有共同点:就是实现了父类或接口的方法,而多态最核心的就是这些被重写或者被实现方法在子类中的不同表现形式。
讲多态的实现方法又必须知道的一个概念:向上转型 看一个例子:
1 class ceshi{ 2 public static void main(String[] args) { 3 animal a1 = new animal(); 4 animal a2 = new Dog(); 5 animal a3 = new Cat(); 6 a1.speak(); 7 a2.speak(); 8 a3.speak(); 9 } 10 } 11 class animal{ 12 public void speak() { 13 System.out.println("this is an animal"); 14 } 15 } 16 17 class Dog extends animal{ 18 @Override 19 public void speak() { 20 System.out.println("this is a Dog"); 21 } 22 } 23 class Cat extends animal{ 24 @Override 25 public void speak() { 26 System.out.println("this is a Cat"); 27 } 28 }
这里我们有一个基类animal,Dog类和Cat类继承了animal类并且Override其speak方法,这里我们可以称Dog和Cat是animal的子类(导出类),注意此时的mian函数的三个定义方法,第一个是正常的new了一个animal对象,而第4行和第5行就是向上转型,可以思考一下输出是什么
this is an animal this is a Dog this is a CatView Code
神奇的事情发生了,我们的引用变量明明定义的animal类,而实际执行的方法却是对应子类的方法,这样JVM的机制有关,具体可以见这篇文章:
https://www.cnblogs.com/kaleidoscope/p/9790766.html
而这就是我们所要介绍的向上转型,左边定义的基类的类别,而实际执行的是右边子类的方法,向上转型就是这样一个特点
对于:
animal a2 = new Dog();
左边是编译类型,其编译类型为animal,就是告诉编译器我定义了一个annimal类型的引用变量,我将要指向一个对象
右边是运行类型,其运行类型是Dog,这就是我们上面说的动态绑定机制,只在真正运行的时候去找具体实现的方法,而现在找到了运行类型是Dog,所以执行Dog.speak
那再总结一下发生了什么:
1、Dog继承了animal
2、Dog重写了animal的speak方法
3、在main函数里出现了向上转型(也就是一个基类的引用指向了一个子类的对象)
这样几个条件,多态发生了,也正是多态机制使一个基类的引用指向了一个子类的对象的方式让编译器承认了
同样这里需要注意第二个Dog重写了speak方法,那万一speak是静态方法呢?会出现什么情况?
如果你去思考运行结果就掉入了思维陷阱,静态方法是不能被重写了,静态代表它只有一份,不允许除它之外的对象去重写,所以speak是静态方法,尝试重写一定会报错。
那再思考一个问题:如果没有重写方法会怎么样?看如下代码:
1 class ceshi{ 2 public static void main(String[] args) { 3 animal a1 = new animal(); 4 animal a2 = new Dog(); 5 animal a3 = new Cat(); 6 a1.speak(); 7 a2.speak(); 8 a3.speak(); 9 } 10 } 11 class animal{ 12 public void speak() { 13 System.out.println("this is an animal"); 14 } 15 } 16 17 class Dog extends animal{ 18 19 } 20 class Cat extends animal{ 21 22 }
可以看到,我们把子类里面的speak方法都删除了,这就与继承的特性相关了,思考一下结果是什么?
this is an animal this is an animal this is an animalView Code
这里发生的事情就是,我们的运行类型是Dog或者Cat,运行时首先去这两个子类里去找speak方法,如果发现找不到,就会向上去找,到父类是animal去找,发现找到了就会直接执行,这个向上的过程是可以无限向上了,直到找到或者到Object类才会结束。
那我们再思考一个问题,我们前面讨论的问题中子类调用的是重写父类的方法,那向上转型后的引用变量能调用子类特有的方法么?如:
1 class ceshi{ 2 public static void main(String[] args) { 3 animal a2 = new Dog(); 4 animal a3 = new Cat(); 5 a2.eat();//出错 6 a3.eat();//出错 7 } 8 } 9 class animal{ 10 public void speak() { 11 System.out.println("this is an animal"); 12 } 13 } 14 15 class Dog extends animal{ 16 public void eat(){ 17 System.out.println("Dog eat"); 18 } 19 20 } 21 class Cat extends animal{ 22 public void eat(){ 23 System.out.println("Cat eat"); 24 } 25 }
我们可以看到我们用向上转型的引用变量是无法调用Dog和Cat类特有的方法eat,也就是第五行和第六行会报错,我们看一下它错误的原因:
我们惊讶的发现,编译器说无法到animal中找到eat方法,我们前面说过,对于一个:
animal a2 = new Dog();
这样定义的引用变量a2,其编译类型是左边的animal,其运行类型是右边的Dog,从上面的结果来看,编译器先确定了我们总共的方法有哪些,就是从编译类型中确定了方法的多少,也就是运行类型中特有的方法编译器是找不到的。
那我们可以下一个结论:向上转型的引用变量能调用的方法的数量(种类)是由编译类型(左边的)决定的,而到具体执行,先看子类有没有重写,如果重写就调用子类,没有就往上追溯,也就是实际方法的实现调用顺序是从右边的运行类型开始,找到就调用,没找到就往上(父类)去找这个方法。
这就是向上转型的全貌,那可能想问?这有什么意义么?我引用它处的一段,真正还得自己使用了去体会,如下:
1.可替换性(substitutability)。多态对已存在代码具有可替换性。例如,多态对圆Circle类工作,对其他任何圆形几何体,如圆环,也同样工作。
2.可扩充性(extensibility)。多态对代码具有可扩充性。增加新的子类不影响已存在类的多态性、继承性,以及其他特性的运行和操作。实际上新加子类更容易获得多态功能。例如,在实现了圆锥、半圆锥以及半球体的多态基础上,很容易增添球体类的多态性。
3.接口性(interface-ability)。多态是超类通过方法签名,向子类提供了一个共同接口,由子类来完善或者覆盖它而实现的。如图8.3
所示。图中超类Shape规定了两个实现多态的接口方法,computeArea()以及computeVolume()。子类,如Circle和Sphere为了实现多态,完善或者覆盖这两个接口方法。
4.灵活性(flexibility)。它在应用中体现了灵活多样的操作,提高了使用效率。
5.简化性(simplicity)。多态简化对应用软件的代码编写和修改过程,尤其在处理大量对象的运算和操作时,这个特点尤为突出和重要。
上面说完了向上转型,其中的一个特点就是引用变量能调用方法的种类是由编译器决定的,我们无法调用子类特有的方法,那当我们真的想调用子类中特有的方法都时候,就可以采用向下转型的方式,如:
1 class ceshi{ 2 public static void main(String[] args) { 3 animal a2 = new Dog(); 4 animal a3 = new Cat(); 5 Dog a4 = (Dog) a2;//向下转型 6 a4.eat();//调用 7 a2 = (animal)a4;//转回animal 8 a2.speak(); 9 } 10 } 11 class animal{ 12 public void speak() { 13 System.out.println("this is an animal"); 14 } 15 } 16 17 class Dog extends animal{ 18 public void speak() { 19 System.out.println("this is an Dog"); 20 } 21 public void eat(){ 22 System.out.println("Dog eat"); 23 } 24 25 } 26 class Cat extends animal{ 27 public void eat(){ 28 System.out.println("Cat eat"); 29 } 30 } 结果: Dog eat this is an Dog
观察第四行和第五行就可以发现,我们将a2的编译类型转成了Dog并赋给了a4,a4成功调用了Dog的独有方法eat(),而a4调用完eat后,同样也可以将编译类型重转为animal,这样a4就是一个向上转型后的引用变量,编译类型变成了animal,运行类型还是Dog。
可以观察出一个规律,我们用类型转换的时候只能修改它的编译类型,而运行类型早在定义的时候就已经固定了(本质上是地址已经固定)。