多态(Polymorphism)属于面向对象三大特征之一,它的前提是封装形成独立体,独立体之间存在继承关系,从而产生多态机制。多态是同一个行为具有多个不同表现形式或形态的能力。现实中,比如我们按下 F1 键这个动作:
● 如果当前在 Flash 界面下弹出的就是 AS 3 的帮助文档;
● 如果当前在 Word 下弹出的就是 Word 帮助;
● 如果当前在 Windows 下弹出的就是 Windows 帮助和支持。
多态就是“同一个行为”发生在“不同的对象上”会产生不同的效果。那么在java中多态是如何体现的呢
在java中允许这样的两种语法出现,一种是向上转型(Upcasting),一种是向下转型(Downcasting),向上转型是指子类型转换为父类型,又被称为自动类型转换,向下转型是指父类型转换为子类型,又被称为强制类型转换。请看下图:
图13-4:向上转型和向下转型
在java语言中有这样的一个规定,无论是向上转型还是向下转型,两种类型之间必须要有继承关系,没有继承关系情况下进行向上转型或向下转型的时候编译器都会报错,这一点要死记硬背哦!
接下来我们来看一段代码:
public class Animal {
public void move(){
System.out.println("Animal move!");
}
}
public class Cat extends Animal{
//方法覆盖
public void move(){
System.out.println("走猫步!");
}
//子类特有
public void catchMouse(){
System.out.println("抓老鼠!");
}
}
public class Bird extends Animal{
//方法覆盖
public void move(){
System.out.println("鸟儿在飞翔!");
}
//子类特有
public void sing(){
System.out.println("鸟儿在歌唱!");
}
}
public class Test01 {
public static void main(String[] args) {
//创建Animal对象
Animal a = new Animal();
a.move();
//创建Cat对象
Cat c = new Cat();
c.move();
//创建鸟儿对象
Bird b = new Bird();
b.move();
}
}
运行结果如下图所示:
图13-5:运行结果
其实在java中还允许这样写代码,请看:
public class Test02 {
public static void main(String[] args) {
Animal a1 = new Cat();
a1.move();
Animal a2 = new Bird();
a2.move();
}
}
运行结果如下图所示:
图13-6:运行结果
以上程序演示的就是多态,多态就是“同一个行为(move)”作用在“不同的对象上”会有不同的表现结果。java中之所以有多态机制,是因为java允许一个父类型的引用指向一个子类型的对象。也就是说允许这种写法:Animal a2 = new Bird(),因为Bird is a Animal是能够说通的。其中Animal a1 = new Cat()或者Animal a2 = new Bird()都是父类型引用指向了子类型对象,都属于向上转型(Upcasting),或者叫做自动类型转换。
我来解释一下这段代码片段【Animal a1 = new Cat();a1.move(); 】:java程序包括编译和运行两个阶段,分析java程序一定要先分析编译阶段,然后再分析运行阶段,在编译阶段编译器只知道a1变量的数据类型是Animal,那么此时编译器会去Animal.class字节码中查找move()方法,发现Animal.class字节码中存在move()方法,然后将该move()方法绑定到a1引用上,编译通过了,这个过程我们可以理解为“静态绑定”阶段完成了。紧接着程序开始运行,进入运行阶段,在运行的时候实际上在堆内存中new的对象是Cat类型,也就是说真正在move移动的时候,是Cat猫对象在移动,所以运行的时候就会自动执行Cat类当中的move()方法,这个过程可以称为“动态绑定”。但无论是什么时候,必须先“静态绑定”成功之后才能进入“动态绑定”阶段。
来看以下的一段代码以及编译结果:
public class Test03 {
public static void main(String[] args) {
Animal a = new Cat();
a.catchMouse();
}
}
编译结果:
图13-7:编译错误信息
有人认为Cat猫是可以抓老鼠的呀,为什么会编译报错呢?那是因为“Animal a = new Cat();”在编译的时候,编译器只知道a变量的数据类型是Animal,也就是说它只会去Animal.class字节码中查找catchMouse()方法,结果没找到,自然“静态绑定”就失败了,编译没有通过。就像以上描述的错误信息一样:在类型为Animal的变量a中找不到方法catchMouse()。
那么,假如说我就是想让这只猫去抓老鼠,以上代码应该如何修改呢?请看以下代码:
public class Test04 {
public static void main(String[] args) {
//向上转型
Animal a = new Cat();
//向下转型:为了调用子类对象特有的方法
Cat c = (Cat)a;
c.catchMouse();
}
}
运行结果如下图所示:
图13-8:向下转型
我们可以看到直接使用a引用是无法调用catchMouse()方法的,因为这个方法属于子类Cat中特有的行为,不是所有Animal动物都可以抓老鼠的,要想让它去抓老鼠,就必须做向下转型(Downcasting),也就是使用强制类型转换将Animal类型的a引用转换成Cat类型的引用c(Cat c = (Cat)a;),使用Cat类型的c引用调用catchMouse()方法。
通过这个案例,可以得出:只有在访问子类型中特有数据的时候,需要先进行向下转型。其实向下转型就是用在这种情形之下。那么向下转型会存在什么风险吗?请看以下代码:
public class Test05 {
public static void main(String[] args) {
Animal a = new Bird();
Cat c = (Cat)a;
}
}
以上代码可以编译通过吗?答案是可以的,为什么呢?那是因为编译器只知道a变量是Animal类型,Animal类和Cat类之间存在继承关系,所以可以进行向下转型(前面提到过,只要两种类型之间存在继承关系,就可以进行向上或向下转型),语法上没有错误,所以编译通过了。但是运行的时候会出问题吗,因为毕竟a引用指向的真实对象是一只小鸟。来看运行结果:
图13-9:类型转换异常
以上的异常是很常见的ClassCastException,翻译为类型转换异常,这种异常通常出现在向下转型的操作过程当中,当类型不兼容的情况下进行转型出现的异常,之所以出现此异常是因为在程序运行阶段a引用指向的对象是一只小鸟,然后我们要将一只小鸟转换成一只猫,这显然是不合理的,因为小鸟和猫之间是没有继承关系的。为了避免这种异常的发生,建议在进行向下转型之前进行运行期类型判断,这就需要我们学习一个运算符了,它就是instanceof。
instanceof运算符的语法格式是这样的:
(引用 instanceof 类型)
instanceof运算符的运算结果是布尔类型,可能是true,也可能是false,假设(c instanceof Cat)结果是true则表示在运行阶段c引用指向的对象是Cat类型,如果结果是false则表示在运行阶段c引用指向的对象不是Cat类型。有了instanceof运算符,向下转型就可以这样写了:
public class Test05 {
public static void main(String[] args) {
Animal a = new Bird();
if(a instanceof Cat){
Cat c = (Cat)a;
c.catchMouse();
}
}
}
以上程序运行之后不再发生异常,并且什么也没有输出,那是因为if语句的条件并没有成立,因为在运行阶段a引用指向的对象不是Cat类型,所以(a instanceof Cat)是false,自然就不会进行向下转型了,也不会出现ClassCastException异常了。在实际开发中,java中有这样一条默认的规范需要大家记住:在进行任何向下转型的操作之前,要使用instanceof进行判断,这是一个很好的编程习惯。就像下面的代码:
public class Test05 {
public static void main(String[] args) {
Animal a = new Bird();
if(a instanceof Cat){
Cat c = (Cat)a;
c.catchMouse();
}else if(a instanceof Bird){
Bird b = (Bird)a;
b.sing();
}
}
}
运行结果如下图所示:
图13-10:向下转型前判断
到这里大家理解什么是多态了吗?其实多态存在的三个必要条件分别是:
● 继承
● 方法覆盖
● 父类型引用指向子类型对象
多态显然是离不开方法覆盖机制的,多态就是因为编译阶段绑定父类当中的方法,程序运行阶段自动调用子类对象上的方法,如果子类对象上的方法没有进行重写,这个时候创建子类对象就没有意义了,自然多态也就没有意义了,只有子类将方法重写之后调用到子类对象上的方法产生不同效果时,多态就形成了。实际上方法覆盖机制和多态机制是捆绑的,谁也离不开谁,多态离不开方法覆盖,方法覆盖离开了多态也就没有意义了。
接下里就来看看之前没有解决的问题:方法覆盖主要是说实例方法,静态方法为什么不谈方法覆盖?
public class OverrideTest {
public static void main(String[] args) {
Math.sum();
MathSubClass.sum();
}
}
public class Math{
public static void sum(){
System.out.println("Math's sum execute!");
}
}
public class MathSubClass extends Math{
//尝试覆盖从父类中继承过来的静态方法
public static void sum(){
System.out.println("MathSubClass's sum execute!");
}
}
运行结果如下图所示:
图13-11:尝试覆盖静态方法
我们发现貌似也发生了覆盖,在程序运行的时候确实也调用了“子类MathSubClass”的sum方法,但这种“覆盖”有意义吗?其实上面的课程我们已经说过了,方法覆盖和多态机制联合起来才有意义,我们来看看这种“覆盖”是否能够达到“多态”的效果,请看代码:
public class OverrideTest {
public static void main(String[] args) {
Math m = new MathSubClass();
m.sum();
m = null;
m.sum();
}
}
运行结果如下图所示:
图13-12:运行结果
通过以上的代码,我们发现虽然创建了子类型对象“new MathSubClass()”,但是程序在运行的时候仍然调用的是Math类当中的sum方法,甚至m = null的时候再去调用m.sum()也没有出现空指针异常,这说明静态方法的执行压根和对象无关,既然和对象无关那就表示和多态无关,既然和多态无关,也就是说静态方法的“覆盖”是没有意义的,所以通常我们不谈静态方法的覆盖。
以上学习了多态的基础语法,多态在实际开发中有什么作用呢?我们先来了解一个业务背景:请设计一个系统,描述主人喂养宠物的场景,首先在这个场景当中应该有“宠物对象”,宠物对象应该有一个吃的行为,另外还需要一个“主人对象”,主人对象应该有一个喂的行为,请看代码:
//宠物狗
public class Dog {
String name;
public Dog(String name){
this.name = name;
}
//吃的行为
public void eat(){
System.out.println(this.name + "在啃肉骨头!");
}
}
//主人
public class Master {
//喂养行为
public void feed(Dog dog){
//主人喂养宠物,宠物就吃
System.out.println("主人开始喂食儿");
dog.eat();
System.out.println("主人喂食儿完毕");
}
}
public class Test {
public static void main(String[] args) {
//创建狗对象
Dog dog = new Dog("二哈");
//创建主人对象
Master master = new Master();
//喂养
master.feed(dog);
}
}
运行结果如下图所示:
图13-13:运行结果
以上程序编译和运行都很正常,输出结果也是对的,那么存在什么问题吗?假设后期用户提出了新的需求,软件可能面临着功能扩展,这个扩展会很方便吗?假设现在主人家里又来了一个宠物猫,那该怎么办呢?请看代码:
在以上代码的基础之上,新增了一个Cat类,来表示宠物猫,这个对于程序来说是可以接受的:
//宠物猫
public class Cat {
String name;
public Cat(String name){
this.name = name;
}
//吃的行为
public void eat(){
System.out.println(this.name + "在吃鱼!");
}
}
另外,除了增加一个Cat类之外,我们还需要“修改”Master主人类的源代码,这件事儿是我们程序员无法容忍的,因为修改之前写好的源代码就面临着重新编译、重新全方位的测试,这是一个巨大的工作,维护成本很高,也很麻烦:
//主人
public class Master {
//喂养行为
public void feed(Dog dog){
//主人喂养宠物,宠物就吃
System.out.println("主人开始喂食儿");
dog.eat();
System.out.println("主人喂食儿完毕");
}
//喂养行为
public void feed(Cat cat){
//主人喂养宠物,宠物就吃
System.out.println("主人开始喂食儿");
cat.eat();
System.out.println("主人喂食儿完毕");
}
}
public class Test {
public static void main(String[] args) {
//创建狗对象
Dog dog = new Dog("二哈");
//创建主人对象
Master master = new Master();
//喂养
master.feed(dog);
//创建猫对象
Cat cat = new Cat("汤姆");
//喂养
master.feed(cat);
}
}
运行结果如下图所示:
图13-14:运行结果
在软件开发过程中,有这样的一个开发原则:开闭原则。开闭原则(OCP)是面向对象设计中“可复用设计”的基石,是面向对象设计中最重要的原则之一,其它很多的设计原则都是实现开闭原则的一种手段。1988年,勃兰特·梅耶(Bertrand Meyer)在他的著作《面向对象软件构造(Object Oriented Software Construction)》中提出了开闭原则,它的原文是这样:“Software entities should be open for extension,but closed for modification”。翻译过来就是:“软件实体应当对扩展开放,对修改关闭”。这句话说得略微有点专业,我们把它讲得更通俗一点,也就是:软件系统中包含的各种组件,例如模块(Modules)、类(Classes)以及功能(Functions)等等,应该在不修改现有代码的基础上,引入新功能。开闭原则中“开”,是指对于组件功能的扩展是开放的,是允许对其进行功能扩展的;开闭原则中“闭”,是指对于原有代码的修改是封闭的,即修改原有的代码对外部的使用是透明的。
以上程序在扩展的过程当中就违背了OCP原则,因为在扩展的过程当中修改了已经写好的Master类,怎样可以解决这个问题呢?多态可以解决,请看代码:
//宠物类
public class Pet {
String name;
//吃的行为
public void eat(){
}
}
//宠物猫
public class Cat extends Pet{
public Cat(String name){
this.name = name;
}
//吃的行为
public void eat(){
System.out.println(this.name + "在吃鱼!");
}
}
//宠物狗
public class Dog extends Pet{
public Dog(String name){
this.name = name;
}
//吃的行为
public void eat(){
System.out.println(this.name + "在啃肉骨头!");
}
}
//主人
public class Master {
//喂养行为
public void feed(Pet pet){
//主人喂养宠物,宠物就吃
System.out.println("主人开始喂食儿");
pet.eat();
System.out.println("主人喂食儿完毕");
}
}
public class Test {
public static void main(String[] args) {
//创建狗对象
Dog dog = new Dog("二哈");
//创建主人对象
Master master = new Master();
//喂养
master.feed(dog);
//创建猫对象
Cat cat = new Cat("汤姆");
//喂养
master.feed(cat);
}
}
运行结果如下图所示:
图13-15:使用多态机制
在以上程序中,Master类中的方法feed(Pet pet)的参数类型定义为更加抽象的Pet类型,而不是具体Dog宠物,或者Cat宠物,显然Master类和具体的Dog、Cat类解耦合了,依赖性弱了,这就是我们通常所说的面向抽象编程,尽量不要面向具体编程,面向抽象编程会让你的代码耦合度降低,扩展能力增强,从而符合OCP的开发原则。假如说这会再来一个新的宠物猪呢,我们只需要这样做,新增加一个“宠物猪类”,然后宠物猪类Pig继承宠物类Pet,并重写eat()方法,然后修改一下测试类就行了,整个过程我们是不需要修改Master类的,只是额外增加了一个新的类:
public class Pig extends Pet {
public Pig(String name){
this.name = name;
}
//吃的行为
public void eat(){
System.out.println(this.name + "在吃粥!");
}
}
public class Test {
public static void main(String[] args) {
//创建狗对象
Dog dog = new Dog("二哈");
//创建主人对象
Master master = new Master();
//喂养
master.feed(dog);
//创建猫对象
Cat cat = new Cat("汤姆");
//喂养
master.feed(cat);
//创建宠物猪对象
Pig pig = new Pig("小猪猪");
master.feed(pig);
}
}
运行结果如下图所示:
图13-16:运行结果
以上程序中到底哪里使用了多态机制呢?请看下图:
图13-17:哪里使用了多态机制
通过以上内容的学习,我们可以看到多态在开发中联合方法覆盖一起使用,可以降低程序的耦合度,提高程序的扩展力。在开发中尽可能面向抽象编程,不要面向具体编程,好比电脑主板和内存条的关系一样,主板和内存条件之间有一个抽象的符合某个规范的插槽,不同品牌的内存条都可以插到主板上使用,2个G的内存条和4个G的内存条都可以插上,但最终的表现结果是不同的,2个G的内存条处理速度慢一些,4个G的快一些,这就是多态,所谓多态就是同一个行为作用到不同的对象上,最终的表现结果是不同的,主要的要求就是对象是可以进行灵活切换的,灵活切换的前提就是解耦合,解耦合依赖多态机制。