前言
咱们晓得,面向对象有三大特色:封装、继承和多态。当初咱们曾经理解了封装和继承,接下来在本文中,给大家带来面向对象的第三大特色:多态。
在这篇文章中,咱们要弄清楚多态的含意、特点、作用,以及如何用代码进行实现。全文大概【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 办法,不会调用子类的;
● 多态使得代码更加灵便,不便了代码扩大。