前言我们知道,面向对象具有三大特性:封装、继承和多态。既然我们已经了解了封装和继承,那么在这篇文章中,我们将为您带来面向对象的第三大特性:多态性。在这篇文章中,我们想搞清楚多态的含义、特点、作用,以及如何用代码来实现。全文约【6000】字,废话不多说,纯干货,让你学技术,懂原理!这篇文章有丰富的案例和图片,让你更好的理解和使用文章中的技术概念,也能给你带来足够的启发性思考1.多态性简介多态性(polymorphism)最初是一个生物表达的概念地球上生命在形式和状态方面的多样性。在Java的面向对象中,多态指的是同一行为能够有多种不同的表现形式。也就是说,父类中定义的属性和方法在子类继承后可以具有不同的数据类型或表现出不同的行为。这可以使同一个属性或方法,在父类及其各个子类中,可能有不同的表现或意义。例如,对于同一个接口,我们在使用不同的实例对象时可能会有不同的操作,在不同的实例对象上发生相同的事件会产生不同的结果。当然,如果只看这么一个干巴巴的概念,你可能还是有点懵,我举个栗子。我们都听过“龙生九子”的故事。大儿子是囚犯,喜欢做音乐;二儿子是亚子,喜欢打架。他们的身后是喜欢冒险爬高的小风,喜欢大喊大叫的普劳,喜欢抽烟的苏安妮,喜欢举重的霸侠,喜欢打官司的毕妍,喜欢温柔的熊,还有会救火的奇奇.都是龙之子,自然都是龙,只是每条龙的性格和技能都不一样。如果有一天玉皇大帝对龙王说:“让你的儿子教我一招。”你认为这个任务的执行结果会怎样?难道要看龙王请哪个儿子来炫耀!如果是给老板表演,就是放音乐;如果是为了老二表演,那就是表演和打架。运行一个方法可能会有不同的结果,这就是多态!功能根据多态的概念,多态机制可以让多个子类在不修改父类代码的情况下扩展自己的功能。比如在父类中定义了一个方法A,N个子类继承父类,所有这些子类都可以重写A方法。并且子类的方法也可以将其参数类型更改为父方法的参数类型,或者将其返回值类型更改为父方法的返回值类型。这样可以动态调整对象的调用,减少对象之间的依赖关系,消除类型之间的耦合,可以很好的扩展程序,通用处理所有类的对象,使代码实现更加灵活简洁。分类Java中的多态性分为编译时多态性和运行时多态性。●编译时多态性:主要通过方法重载来实现。Java会根据方法的参数列表来区分不同的方法,在编译时就可以决定应该执行哪些重载的方法。这就是静态多态,也叫静态多态、静态绑定、前向绑定。但是也有一种特殊情况是方法重写,属于编译期多态。重写方法时,当对象的引用指向当前对象所属类的对象时,也是编译期多态,因为在编译阶段就可以确定执行的方法属于哪个对象.●运行时多态性:主要通过方法覆盖实现,让子类继承父类,重写父类中已有的或抽象的方法。这就是动态多态,也叫“后绑定”,也就是我们通常所说的多态。一句话,如果我们能在编译时判断出要执行的方法属于哪个对象,执行哪个方法,这就是编译时多态,否则就是运行时多态!特点根据多态性的要求,Java对象的类型可以分为编译型和运行型。多态性具有以下特点:●一个对象的编译类型可以和运行类型不一致;●编译类型在定义对象时确定。改变,但运行类型可以改变;●编译类型取决于定义对象时=号的左边,运行类型取决于=号的右边。所以当我们以多态的方式调用方法时,首先会检查是否有这个方法,如果没有,就会产生编译错误;如果有,则调用子类中的同名方法。即编译时依赖父类,运行时依赖子类。必要条件要实现多态,需要满足三个必要条件:●继承:多态发生在继承关系中,必须存在于具有继承关系的父类和子类中。多态是基于上面的封装和继承;●重写:方法重写是必须的,子类重定义父类的一些方法;●向上转型:就是将父类的引用指向子类对象,只有这样引用才能调用父类的方法,才能调用子类的方法。只有满足以上三个条件,才能实现多态,开发者可以使用统一的代码实现,在同一个继承结构中处理不同的对象,从而做出不同的行为。2、多态的实现方法在Java中,多态的实现有以下几种方式:方法重载:重载可以在编译时根据实参的数据类型、个数和顺序来决定。哪一个。●方法重写:该方法是基于方法重写的多态性;●接口实现:接口是一种不能实例化但可以实现的抽象类型,是抽象方法的集合。定义一个接口可以有多个实现,这也是多态的一种形式,类似于继承中的方法重写。实现过程2.1需求分析现在我们有一个需求:客户要求我们为他生产设备。他需要更多种类的产品。他可能需要圆形的设备,或者三角形、长方形等各种形状的设备。我们应该如何生产它?如果按照我们之前的经验,我们可以创建圆形、三角形、矩形等,每一种都有相应的生产方法,负责生产相应的产品。但如果这样设计,不符合面向对象的要求。将来,客户可能会有许多其他要求。如果我们为每个需求设计一个类和方法,最终我们的项目代码会非常冗长。其实在客户的这些需求中,有很多共同的需求!比如无论客户需要什么形状,我们都要进行“图纸制作”。在拉图制作的过程中,可能用到的材料都是一样的,只是形状不同而已!这就像生产巧克力。有圆形的,方形的,还有奇形怪状的。不管怎样,基本的原料都是巧克力。既然如此,总不能每一种装备都从头到尾吧?那么既然它们有很多相同的内容,我们就可以定义一个通用的父类,在父类中完成通用的功能和特性,然后子类继承父类,每个子类都会进行扩展,实现自己的个性化功能。如下图所示:这就是符合面向对象特性的代码设计!接下来,易哥将通过一些代码案例来为大家展示如何实现这个需求。2.2代码实现接下来将通过实现接口的方法来演示多态代码实现过程。方法重载和方法重写的方法其实在上一篇文章中已经讲解过了,这里不再赘述。2.2.1定义Shape接口我们首先定义一个Shape接口,它是一个父类。在Java中,子类可以继承父类或实现接口。一个子类只能继承一个父类,但可以实现多个接口。这些接口属于子类的“间接父类”,你可以理解为子类的“教父”或祖父母。接口的内容会在后面的文章中具体讲解,敬请期待,大家可以先用到这里。2.2.2定义Circle类定义一个Circle子类来实现Shape接口。请注意,我们在这里使用了implements关键字!2.2.3定义Traingle类,再定义一个Traingle子类,同样实现了Shape接口。2.2.4定义Square类,最后定义一个Square子类,同样实现了Shape接口。(2.4.5定义测试类确定了父子关系之后,再定义一个额外的测试类,在这个测试类中,我们创建了上面三个图形对象。注意=等号左边,变量的类型是Shape的父类;=等号右边,变量的值是具体的子类!这种变量的定义过程其实满足了多态的第三个必要条件,即所谓“向上转化,父类引用指向子类对象”。我们可以看出,上面的代码满足了多态的三个必要条件:继承、再转化、向上转化!有子类继承了父类,方法重写,向上改造。而根据这个案例,我们可以进一步理解多态的含义和特点。在多态中,对于某一类型的方法调用,实际执行的方法取决于方法的实际类型在运行时!本案例最终执行结果如下图所示:2.3结果分析在上面的案例中,我们有如下一行代码:在上面的代码中,我们实际的类型是Circle,Traingle,Square,以及它们的共同父类类,其引用类型是Shape变量。当我们调用shape.draw()的时候,大家可以想一想,执行的是父类Shape的draw()方法还是具体子类的draw()方法?大部分同学应该都能猜到,应该执行具体子类的draw()方法!基于以上案例,我们可以得出一个结论:Java实例方法的调用是基于运行时实际类型的动态调用,而不是声明的变量类型!通俗地说,就是我们调用哪个对象的方法,不是由=号左边决定的,是由这边声明的引用变量决定的,而是由=号右边的实际对象类型决定的!这也是多态的一个重要特征!所以我们说在多态中,对于某个类型的方法调用,其实际执行的方法取决于运行时方法的实际类型!也就是说,只有在运行时,我们才能动态决定调用哪个子类方法。这个不确定的方法调用的作用是什么?其实主要是让我们在不修改父类代码的情况下,通过添加更多类型的子类来扩展父类的功能。3.重写扩展补充方法时的编译期多态当对象的引用指向当前对象所属的类时对象,即使是方法重写,仍然是编译期多态的。1.1定义父类我们先定义一个Father父类,内部定义一个eat()方法。1.2定义子类再定义一个Son子类继承Father父类重写eat()方法。这里的Son子类虽然继承了父类Father并重写了父类的方法,但是对象的引用指向的是当前对象所属类的对象,即son引用指向的是newSon()对象,这也是编译时多态!实现多态时的几个细节2.1定义Father父类我们定义一个Father父类,它定义了name属性,成员方法eat(),静态方法play()。2.2定义Son子类再定义一个Son子类,其中定义了同名的name属性和特有的age属性,重写了成员方法eat(),特有的drink()方法,定义了一个静态方法playwiththe一样的名字()。2.3执行结果上面代码的执行结果如下图所示:根据上面代码的执行结果,当父类引用指向子类对象时,父类只能调用和执行子类声明的方法在父类中,被子类覆盖,但不能执行子类特有的成员方法。否则在编译阶段会出现“Themethoddrink()isundefinedforthetypeFather”异常。另外,当子类和父类的属性相同时,父类会调用自己的属性。当父类引用指向子类对象向上转型时,如果父类调用子类特有的属性,编译时会报错“agecannotberesolvedorisnotafield”。如果在Father的父类中定义了一个静态方法play(),在子类中定义了一个同名的静态方法play(),则上面代码中的son.play()执行的是父类中的play()方法父级。向上转型时,当父类引用调用同名静态方法时,执行父类中的方法。这是因为在运行时,虚拟机已经确定了静态方法属于哪个类。“方法覆盖”仅适用于实例方法,不适用于静态方法。静态方法只能被隐藏、重载和继承,不能被覆盖。子类会隐藏父类的静态方法,但不能重写父类的静态方法,所以子类的静态方法不能体现多态性,这和子类属性隐藏父类属性是一样的。4.结语至此,我们学习完了面向对象的三大特性。你现在熟悉这三个特征了吗?最后,我们来看一下多态的要点:●多态是指不同子类型的对象,对相同的行为做出不同的反应;●要实现多态,必须满足继承、再过渡、向上转型的条件;●多态性分为编译时多态性和运行时多态性。我们常说的多态指的是运行时多态;方法重载是编译时多态,方法重写是运行时多态,但是重写有Exception;●当父类引用指向子类对象时,调用的实例方法是子类重写的方法,父类引用不能调用子类的新方法和子类特定属性;●父类引用指向子类对象时,父类引用只会调用父类自己的属性和静态方法,不会调用子类的;●多态性使代码更加灵活,便于代码扩展。
