前言
我们知道,面向对象有三大特征:封装、继承和多态。现在我们已经了解了封装和继承,接下来在本文中,给大家带来面向对象的第三大特征:多态。
在这篇文章中,我们要弄清楚多态的含义、特点、作用,以及如何用代码进行实现。全文大约【6000】字,不说废话,只讲可以让你学到技术、明白原理的纯干货!本文带有丰富的案例及配图,让你更好地理解和运用文中的技术概念,并可以给你带来具有足够启迪的思考
一. 多态简介
概念多态(polymorphism)本来是生物学里的概念,表示地球上的生物在形态和状态方面的多样性。
而在java的面向对象中,多态则是指同一个行为可以有多个不同表现形式的能力。也就是说,在父类中定义的属性和方法,在子类继承后,可以有不同的数据类型或表现出不同的行为。这可以使得同一个属性或方法,在父类及其各个子类中,可能会有不同的表现或含义。比如针对同一个接口,我们使用不同的实例对象可能会有不同的操作,同一事件发生在不同的实例对象上会产生不同的结果。
当然,如果我们只是看这样干巴巴的概念,可能大家还是有点懵,给大家举个栗子。
我们都听过“龙生九子”的故事。长子是囚牛,喜欢搞音乐;次子是睚眦,喜欢打架。后面还有喜欢冒险登高的嘲风,爱大喊大叫的蒲牢,喜欢吸烟的狻猊,爱好举重的霸下,好打官司的狴犴,喜欢斯文的负屃,会灭火的螭吻。他们都是龙的儿子,自然也都是龙,但每个龙都有不同的个性和技能。假如有一天玉帝对龙王说,“让你的儿子来给我秀个技能”。大家说这个任务的执行结果会怎么样?这是不是得看龙王让哪个儿子来秀了!如果是让老大来表演,就是演奏音乐;如果是让老二来表演,就是表演打架
从这个故事中,我们就可以感受到,九个龙子虽然都继承了共同的父类,但子类在运行某个方法时却可能会有不同的结果,这就是多态!
作用
根据多态的概念可知,多态机制可以在不修改父类代码的基础上,允许多个子类进行功能的扩展。比如父类中定义了一个方法A,有N个子类继承该父类,这几个子类都可以重写这个A方法。并且子类的方法还可以将自己的参数类型改为父类方法的参数类型,或者将自己的返回值类型改为父类方法的返回值类型。这样就可以动态地调整对象的调用,降低对象之间的依存关系,消除类型之间的耦合,使程序有良好的扩展,并可以对所有类的对象进行通用处理,让代码实现更加的灵活和简洁。
分类
Java中的多态,分为编译时多态和运行时多态。
● 编译时多态:主要是通过方法的重载(overload)来实现,Java会根据方法参数列表的不同来区分不同的方法,在编译时就能确定该执行重载方法中的哪一个。这是静态的多态,也称为静态多态性、静态绑定、前绑定。但也有一种特殊的方法重写的情况,属于编译时多态。在方法重写时,当对象的引用指向的是当前对象自己所属类的对象时,也是编译时多态,因为在编译阶段就能确定执行的方法到底属于哪个对象。
● 运行时多态:主要是通过方法的重写(override)来实现,让子类继承父类并重写父类中已有的或抽象的方法。这是动态的多态,也称为”后绑定“,这是我们通常所说的多态性。一句话,如果我们在编译时就能确定要执行的方法属于哪个对象、执行的是哪个方法,这就是编译时多态,否则就是运行时多态!
特性
根据多态的要求,Java对象的类型可以分为编译类型和运行类型,多态有如下特性:
● 一个对象的编译类型与运行类型可以不一致;
● 编译类型在定义对象时就确定了,不能改变,而运行类型却是可以变化的;
● 编译类型取决于定义对象时 =号的左边,运行类型取决于 =号的右边
所以我们在使用多态方式调用方法时,首先会检查父类中是否有该方法,如果没有,则会产生编译错误;如果有,再去调用子类中的同名方法。即编译时取决于父类,运行时取决于子类。
必要条件
我们要想实现多态,需要满足3个必要条件:
● 继承:多态发生在继承关系中,必须存在有继承关系的父类和子类中,多态建立在封装和继承的基础之上;
● 重写:必须要有方法的重写,子类对父类的某些方法重新定义;
● 向上转型:就是要将父类引用指向子类对象,只有这样该引用才既能调用父类的方法,又能调用子类的方法。
只有满足了以上3个条件才能实现多态,开发人员也才能在同一个继承结构中,使用统一的代码实现来处理不同的对象,从而执行不同的行为。
二. 多态的实现
实现方式
在Java中,多态的实现有如下几种方式:
● 方法重载:重载可以根据实际参数的数据类型、个数和次序,在编译时确定执行重载方法中的哪一个。
● 方法重写:这种方式是基于方法重写来实现的多态;
● 接口实现:接口是一种无法被实例化但可以被实现的抽象类型,是对抽象方法的集合。定义一个接口可以有多个实现,这也是多态的一种实现形式,与继承中方法的重写类似。
实现过程
2.1 需求分析现在我们有一个需求:有一个客户要求我们给他生产设备器材,他需要的产品类型比较多,可能要圆形的器材,也可能需要三角形、矩形等各种形状的器材,我们该怎么生产实现?
如果是按照我们之前的经验,可以分别创建圆形类、三角形类、矩形类等,里面各自有对应的生产方法,负责生产出对应的产品。但是如果这样设计,其实不符合面向对象的要求。以后客户可能还会有很多其他的需求,如果针对每一个需求都设计一个类和方法,最终我们的项目代码就会很啰嗦。
实际上,在客户的这些需求中,有很多要求是具有共性的!比如,无论客户需要什么形状的器材,我们都要进行”绘制生产“,在绘制生产的过程中,可能用到的材料都是一样的,无非就是形状不同!就好比生产巧克力,有圆的方的奇形怪状的,不管怎么样,基础原料都是巧克力。既然如此,我们总不能针对每一种形状的器材都从头到尾搞一遍吧?
所以既然它们有很多内容都一样,我们就可以定义一个共同的父类,在父类中完成共性的功能和特征,然后由子类继承父类,每个子类再扩展实现自己个性化的功能。如下图所示:
这样就是符合面向对象特征的代码设计了!接下来壹哥就通过一些代码案例,来给大家演示该如何实现这个需求。
2.2 代码实现接下来会采用实现接口的方式来演示多态的代码实现过程。方法重载和方法重写的方式,其实我们在前面的文章中已经有所讲解,这里不再赘述。
2.2.1 定义Shape接口我们首先定义出一个Shape接口,这个接口就是一个父类。在Java中,子类可以继承父类,也可以实现接口。一个子类只能继承一个父类,但是却可以实现多个接口。这些接口,属于是子类的”间接父类“,你可以理解为是子类的”干爹“或者爷爷等祖辈。关于接口的内容,会在后面的文章中专门讲解,敬请期待哦,此处大家先会使用即可。
2.2.2 定义Circle类定义一个Circle子类,实现Shape接口,注意我们这里使用了implements关键字!
述(最多18字2.2.3 定义Traingle类然后再定义一个Traingle子类,也实现Shape接口。
2.2.4 定义Square类最后定义一个Square子类,同样实现Shape接口。
述(2.4.5 定义测试类父子关系确定好之后,接下来我们再定义一个额外的测试类。在这个测试类中,我们创建出以上三个图形对象。注意,在=等号左侧,变量的类型都是Shape父类;=等号右侧,变量的值是具体的子类!这种变量的定义过程,其实就是符合了多态的第三个必要条件,也就是所谓的”向上转型,父类引用指向子类对象“。
我们可以看到上述代码,满足了多态的3个必要条件:继承、重新、向上转型!有子类继承父类,有方法重写,有向上转型。而且根据这个案例,我们可以进一步理解多态的含义和特点。在多态中,针对某个类型的方法调用,其真正执行的方法取决于运行时期实际类型的方法!本案例最终的执行结果如下图所示:
2.3 结果分析在上述案例中,我们有如下一行代码:
上述代码中,我们实际的类型是Circle、Traingle、Square,他们共同的父类,其引用类型是Shape变量。当我们调用shape.draw()时,大家可以想一下,执行的是父类Shape的draw()方法还是具体子类的draw()方法?大多数同学应该能够想出来,执行的应该是具体子类的draw()方法!
基于以上这个案例,我们可以得出一个结论:
Java实例方法的调用,是基于运行时实际类型的动态调用,而非声明的变量类型!通俗地说,就是我们调用的到底是哪个对象的方法,不是由=号左侧声明的引用变量来决定的,而是由=号右侧的实际对象类型来决定的!
这也是多态的一个重要特征!所以我们说在多态中,针对某个类型的方法调用,其真正执行的方法取决于运行时期实际类型的方法!即只有在运行期,才能动态决定调用哪个子类的方法。这种不确定性的方法调用,究竟有什么作用呢?其实主要就是允许我们能够添加更多类型的子类,实现对父类功能的扩展,而不需要修改父类的代码。
三. 扩展补充
方法重写时的编译时多态当对象的引用指向的是当前对象所属类的对象,即使是方法重写,依然属于编译时多态。
1.1 定义父类我们先定义一个Father父类,内部定义一个eat()方法。
1.2 定义子类接着定义一个Son子类继承Father父类,并重写eat()方法
虽然这里的Son子类继承了父类Father,并重写了父类的方法,但对象的引用指向的是当前对象所属类的对象,即son引用指向的是new Son()对象,这也是编译时多态!
实现多态时的若干细节
2.1 定义Father父类我们定义一个Father父类,类中定义了name属性,成员方法eat(),静态方法play()。
2.2定义Son子类接着再定义一个Son子类,类中定义了同名的name属性和特有的age属性,重写成员方法eat(),特有的drink()方法,并定义一个同名的静态方法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方法,不会调用子类的;
● 多态使得代码更加灵活,方便了代码扩展。
服务器托管,北京服务器托管,服务器租用 http://www.fwqtg.net