前言
我们知道,面向对象有三大特征:封装、继承和多态。现在我们已经了解了封装和继承,接下来在本文中,会给大家讲解多态的内容。在这篇文章中,我们要弄清楚多态的含义、特点、作用,以及如何用代码进行实现。
全文大约【6000】字,不说废话,只讲可以让你学到技术、明白原理的纯干货!本文带有丰富的案例及配图,让你更好地理解和运用文中的技术概念,并可以给你带来具有足够启迪的思考
一. 多态简介1. 概念
多态(polymorphism)本来是生物学里的概念,表示地球上的生物在形态和状态方面的多样性。
而在java的面向对象中,多态则是指同一个行为可以有多个不同表现形式的能力。也就是说,在父类中定义的属性和方法,在子类继承后,可以有不同的数据类型或表现出不同的行为。这可以使得同一个属性或方法,在父类及其各个子类中,可能会有不同的表现或含义。比如针对同一个接口,我们使用不同的实例对象可能会有不同的操作,同一事件发生在不同的实例对象上会产生不同的结果。
当然,如果我们只是看这样干巴巴的概念,可能大家还是有点懵,给大家举个栗子。
我们都听过“龙生九子”的故事。长子是囚牛,喜欢搞音乐;次子是睚眦,喜欢打架。后面还有喜欢冒险登高的嘲风,爱大喊大叫的蒲牢,喜欢吸烟的狻猊,爱好举重的霸下,好打官司的狴犴,喜欢斯文的负屃,会灭火的螭吻。他们都是龙的儿子,自然也都是龙,但每个龙都有不同的个性和技能。假如有一天玉帝对龙王说,“让你的儿子来给我秀个技能”。大家说这个任务的执行结果会怎么样?这是不是得看龙王让哪个儿子来秀了!如果是让老大来表演,就是演奏音乐;如果是让老二来表演,就是表演打架…..
从这个故事中,我们就可以感受到,九个龙子虽然都继承了共同的父类,但子类在运行某个方法时却可能会有不同的结果java可变参数,这就是多态!
2. 作用
根据多态的概念可知,多态机制可以在不修改父类代码的基础上,允许多个子类进行功能的扩展。比如父类中定义了一个方法A,有N个子类继承该父类,这几个子类都可以重写这个A方法。并且子类的方法还可以将自己的参数类型改为父类方法的参数类型,或者将自己的返回值类型改为父类方法的返回值类型。这样就可以动态地调整对象的调用,降低对象之间的依存关系,消除类型之间的耦合,使程序有良好的扩展,并可以对所有类的对象进行通用处理,让代码实现更加的灵活和简洁。
3. 分类
Java中的多态,分为编译时多态和运行时多态。
● 编译时多态:主要是通过方法的重载(overload)来实现,Java会根据方法参数列表的不同来区分不同的方法,在编译时就能确定该执行重载方法中的哪一个。这是静态的多态,也称为静态多态性、静态绑定、前绑定。但也有一种特殊的方法重写的情况,属于编译时多态。在方法重写时,当对象的引用指向的是当前对象自己所属类的对象时,也是编译时多态,因为在编译阶段就能确定执行的方法到底属于哪个对象。
● 运行时多态:主要是通过方法的重写(override)来实现,让子类继承父类并重写父类中已有的或抽象的方法。这是动态的多态,也称为”后绑定“,这是我们通常所说的多态性。
一句话,如果我们在编译时就能确定要执行的方法属于哪个对象、执行的是哪个方法,这就是编译时多态,否则就是运行时多态!
4. 特性
根据多态的要求,Java对象的类型可以分为编译类型和运行类型,多态有如下特性:
● 一个对象的编译类型与运行类型可以不一致;
● 编译类型在定义对象时就确定了,不能改变,而运行类型却是可以变化的;
● 编译类型取决于定义对象时 =号的左边,运行类型取决于 =号的右边
所以我们在使用多态方式调用方法时,首先会检查父类中是否有该方法,如果没有,则会产生编译错误;如果有,再去调用子类中的同名方法。即编译时取决于父类,运行时取决于子类。
5. 必要条件
我们要想实现多态,需要满足3个必要条件:
● 继承:多态发生在继承关系中,必须存在有继承关系的父类和子类中,多态建立在封装和继承的基础之上;
● 重写:必须要有方法的重写,子类对父类的某些方法重新定义;
● 向上转型:就是要将父类引用指向子类对象,只有这样该引用才既能调用父类的方法,又能调用子类的方法。
只有满足了以上3个条件才能实现多态,开发人员也才能在同一个继承结构中,使用统一的代码实现来处理不同的对象,从而执行不同的行为。
二. 多态的实现1. 实现方式
在Java中,多态的实现有如下几种方式:
● 方法重载:重载可以根据实际参数的数据类型、个数和次序,在编译时确定执行重载方法中的哪一个。
● 方法重写:这种方式是基于方法重写来实现的多态;
● 接口实现:接口是一种无法被实例化但可以被实现的抽象类型,是对抽象方法的集合。定义一个接口可以有多个实现,这也是多态的一种实现形式,与继承中方法的重写类似。
2. 实现过程2.1 需求分析
现在我们有一个需求:有一个客户要求我们给他生产设备器材,他需要的产品类型比较多,可能要圆形的器材,也可能需要三角形、矩形等各种形状的器材,我们该怎么生产实现?
如果是按照我们之前的经验,可以分别创建圆形类、三角形类、矩形类等,里面各自有对应的生产方法,负责生产出对应的产品。但是如果这样设计,其实不符合面向对象的要求。以后客户可能还会有很多其他的需求,如果针对每一个需求都设计一个类和方法,最终我们的项目代码就会很啰嗦。
实际上,在客户的这些需求中,有很多要求是具有共性的!比如,无论客户需要什么形状的器材,我们都要进行”绘制生产“,在绘制生产的过程中,可能用到的材料都是一样的,无非就是形状不同!就好比生产巧克力,有圆的方的奇形怪状的,不管怎么样,基础原料都是巧克力。既然如此,我们总不能针对每一种形状的器材都从头到尾搞一遍吧?
所以既然它们有很多内容都一样,我们就可以定义一个共同的父类,在父类中完成共性的功能和特征,然后由子类继承父类,每个子类再扩展实现自己个性化的功能。如下图所示:
这样就是符合面向对象特征的代码设计了!接下来就通过一些代码案例,来给大家演示该如何实现这个需求。
2.2 代码实现
接下来会采用实现接口的方式来演示多态的代码实现过程。方法重载和方法重写的方式,其实我们在前面的文章中已经有所讲解,这里不再赘述。
2.2.1 定义Shape接口
我们首先定义出一个Shape接口,这个接口就是一个父类。在Java中,子类可以继承父类,也可以实现接口。一个子类只能继承一个父类,但是却可以实现多个接口。这些接口,属于是子类的”间接父类“,你可以理解为是子类的”干爹“或者爷爷等祖辈。关于接口的内容,会在后面的文章中专门讲解,敬请期待哦java可变参数,此处大家先会使用即可。
/**
*
* 定义一个“图形”接口----属于父类!
*/
public interface Shape {
//绘制方法。接口中的方法一般没有实现,需要子类进行实现。
void draw();
}
2.2.2 定义Circle类
定义一个Circle子类,实现Shape接口,注意我们这里使用了implements关键字!
/**
*
* “圆形”类---实现Shape接口,并对接口中的方法进行实现
*/
public class Circle implements Shape{
@Override
public void draw() {
System.out.println("绘制圆形");
}
//子类中定义了一个独有的方法。
//当父类引用指向子类对象时,父类只能调用执行那些在父类中声明、被子类覆盖的子类方法,
//而不能执行子类独有的成员方法。
public void scroll(){
System.out.println("圆形类独有的方法");
}
}
2.2.3 定义Traingle类
然后再定义一个Traingle子类,也实现Shape接口。
/**
*
* “矩形”类---实现Shape接口,并对接口中的方法进行实现
*/
public class Traingle implements Shape{
@Override
public void draw() {
System.out.println("绘制矩形");
}
}
2.2.4 定义Square类
最后定义一个Square子类,同样实现Shape接口。
/**
*
* “三角形”类---实现Shape接口,并对接口中的方法进行实现
*/
public class Square implements Shape{
@Override
public void draw() {
System.out.println("绘制三角形");
}
}
2.4.5 定义测试类
父子关系确定好之后,接下来我们再定义一个额外的测试类。在这个测试类中,我们创建出以上三个图形对象。注意,在=等号左侧,变量的类型都是Shape父类;=等号右侧,变量的值是具体的子类!这种变量的定义过程,其实就是符合了多态的第三个必要条件,也就是所谓的”向上转型,父类引用指向子类对象“。
public class ShapeTest {
public static void main(String[] args) {
//多态测试
Shape shape01=new Circle();
shape01.draw();
//当父类引用指向子类对象时,父类只能调用执行那些在父类中声明、被子类覆盖的子类方法,
//而不能执行子类独有的成员方法。否则在编译阶段就会出现:
//The method drink() is undefined for the type Father
//shape01.scroll();
Shape shape02=new Traingle();
shape02.draw();
Shape shape03=new Square();
shape03.draw();
}
}
我们可以看到上述代码,满足了多态的3个必要条件:继承、重新、向上转型!有子类继承父类,有方法重写,有向上转型。而且根据这个案例,我们可以进一步理解多态的含义和特点。在多态中,针对某个类型的方法调用,其真正执行的方法取决于运行时期实际类型的方法!
本案例最终的执行结果如下图所示:
2.3 结果分析
在上述案例中,我们有如下一些代码:
Shape shape01=new Circle();
//无法确定运行时究竟调用哪个run()方法
shape01.draw();
Shape shape02=new Traingle();
shape02.draw();
Shape shape03=new Square();
shape03.draw();
上述代码中,我们实际的类型是Circle、Traingle、Square,他们共同的父类,其引用类型是Shape变量。当我们调用shape.draw()时,大家可以想一下,执行的是父类Shape的draw()方法还是具体子类的draw()方法?大多数同学应该能够想出来,执行的应该是具体子类的draw()方法!
基于以上这个案例,我们可以得出一个结论:
Java实例方法的调用,是基于运行时实际类型的动态调用,而非声明的变量类型!通俗地说,就是我们调用的到底是哪个对象的方法,不是由=号左侧声明的引用变量来决定的,而是由=号右侧的实际对象类型来决定的!
这也是多态的一个重要特征!所以我们说在多态中,针对某个类型的方法调用,其真正执行的方法取决于运行时期实际类型的方法!即只有在运行期,才能动态决定调用哪个子类的方法。这种不确定性的方法调用,究竟有什么作用呢?其实主要就是允许我们能够添加更多类型的子类,实现对父类功能的扩展,而不需要修改父类的代码。
三. 扩展补充1. 方法重写时的编译时多态
当对象的引用指向的是当前对象所属类的对象,即使是方法重写,依然属于编译时多态。
1.1 定义父类
我们先定义一个Father父类,内部定义一个eat()方法。
/**
* 定义父类
*/
public class Father {
// 吃
public void eat() {
System.out.println("爹吃馒头");
}
}
1.2 定义子类
接着定义一个Son子类继承Father父类,并重写eat()方法。
public class Son extends Father {
// 吃
@Override
public void eat() {
// 方法重写时,子类可以对父类的同名方法进行扩展实现,方法体的内容可以和父类中的实现不一样
System.out.println("儿子吃肉");
}
public static void main(String[] args) {
//创建出父类对象
Father father=new Father();
father.eat();
//创建子类对象
//虽然子类继承了父类,并重写了父类的方法,但对象的引用指向的是当前对象所属类的对象,即son引用指向的是new Son()对象,这也是编译时多态!
Son son = new Son();
son.eat();
}
}
虽然这里的Son子类继承了父类Father,并重写了父类的方法,但对象的引用指向的是当前对象所属类的对象,即son引用指向的是new Son()对象,这也是编译时多态!
2. 实现多态时的若干细节
2.1 定义Father父类
我们定义一个Father父类,类中定义了name属性,成员方法eat(),静态方法play()。
/**
* 定义父类
*/
public class Father {
// 父类中的成员变量
String name = "老子";
// 吃
public void eat() {
System.out.println("爹吃馒头");
}
// 父类中的静态方法。静态方法不能被重新,只会被子类隐藏!
public static void play() {
System.out.println("爹玩球");
}
}
2.2 定义Son子类
接着再定义一个Son子类,类中定义了同名的name属性和特有的age属性,重写成员方法eat(),特有的drink()方法,并定义一个同名的静态方法play()。
//Son子类
public class Son extends Father {
// 成员变量--变量隐藏
// 变量的类型和名称,都与父类中的成员变量相同
String name = "儿子";
//子类中独有的属性
int age = 10;
// 吃
@Override
public void eat() {
// 方法重写时,子类可以对父类的同名方法进行扩展实现,方法体的内容可以和父类中的实现不一样
System.out.println("儿子吃肉");
}
// 喝---子类新增的方法。
// 当父类引用指向子类对象时,父类只能调用执行那些在父类中声明、被子类覆盖的子类方法,而不能执行子类独有的成员方法。
public void drink() {
System.out.println("儿子吃肉");
}
// 子类中与父类同名的静态方法,这不是重写,而是子类对父类同名静态方法的隐藏!
public static void play() {
System.out.println("儿子玩火");
}
public static void main(String[] args) {
// 当父类引用指向子类对象时,父类只能调用执行那些在父类中声明、被子类覆盖的子类方法,而不能执行子类独有的成员方法。
// Father son=new Son();
// The method drink() is undefined for the type Father
// son.drink();
Father son=new Son();
//当子类和父类有相同属性时,父类会调用自己的属性。
System.out.println("name="+son.name);//老子
//当父类引用指向子类对象向上转型时,若父类调用子类特有的属性,在编译时期就会报错:
//age cannot be resolved or is not a field
//son.age;
//虽然我们不建议通过“对象.方法名”的形式来调用类的静态方法,但这么调用也不会出错。
son.play();//爹玩球
}
}
2.3 执行结果
上述代码执行结果如下图所示:
根据上述代码的执行结果可知,当父类引用指向子类对象时,父类只能调用执行那些在父类中声明、被子类覆盖的子类方法,而不能执行子类独有的成员方法。否则在编译阶段就会出现”The method drink() is undefined for the type Father“异常。
另外当子类和父类有相同属性时,父类会调用自己的属性。当父类引用指向子类对象向上转型时,若父类调用子类特有的属性,在编译时期就会报错”age cannot be resolved or is not a field“。
如果Father父类中定义了一个静态方法play(),子类也定义了一个同名的静态方法play(),上述代码中son.play()执行的是Father类中的play()方法。在进行向上转型时,父类引用调用同名的静态方法时,执行的是父类中的方法。这是因为在运行时,虚拟机已经确定了static方法属于哪个类。“方法重写”只适用于实例方法,对静态方法无效。静态方法,只能被隐藏、重载、继承,但不会被重写。子类会将父类的静态方法隐藏,但不能覆盖父类的静态方法,所以子类的静态方法体现不了多态,这和子类属性隐藏父类属性一样。
四. 结语
至此,我们就把面向对象的三大特征都学习完毕了,现在你对这三大特征都熟悉了吗?最后我们再来看看多态的要点都有哪些吧:
● 多态指的是不同子类型的对象,对同一行为作出的不同响应;
● 实现多态要满足继承、重新、向上转型的条件;
● 多态分为编译时多态和运行时多态,我们常说的多态是指运行时多态;
● 方法重载是编译时多态,方法重写是运行时多态,但重写有例外情况;
● 父类引用指向子类对象时,调用的实例方法是子类重写的方法,父类引用不能调用子类新增的方法和子类特有属性;
● 父类引用指向子类对象时,父类引用只会调用父类自己的属性和static方法,不会调用子类的;
● 多态使得代码更加灵活,方便了代码扩展。
往期推荐:
JAVA面向对象三大特征之——封装
JAVA面向对象的三大特征——继承
Java中的String类真的不可变吗?Java面试常见问题
Java向上转型与向下转型,java基础学习千字干货详解!
Java方法重写(Override)与方法重载(Overlode)的区别详解
限时特惠:本站每日持续更新海量设计资源,一年会员只需29.9元,全站资源免费下载
站长微信:ziyuanshu688