面向对象程序设计有三要素:封装、继承(或组合)、多态,前两者较好理解,多态总让人困惑,不知道具体有什么作用,更不知道为什么要用多态。今天就来详细分析下什么是多态,以及多态有哪些好处,为什么要用多态?
多态是指同一行为作用于不同对象时,可以表现出多种不同的形式和结果来。例如,子类继承父类并覆盖其方法后,用父类引用指向子类对象并调用该方法时,实际执行的是子类的方法。
这种根据对象实际类型而非声明类型来确定执行方法的行为,就是多态性的体现。多态主要通过继承和接口实现,允许同一接口有多种不同的实现方式。
编译时多态,又称静态绑定,是指编译器在编译时通过检查引用类型的方法是否存在,来定位到相应的类及其方法,而不检查实际对象是否支持该方法。编译时多态主要体现在方法重载上,即根据参数类型、数量和顺序,在编译时确定要执行的方法。
运行时多态,又称动态绑定,是指程序在运行时根据对象的实际类型来确定调用哪个方法,而不是在编译时确定。这意味着方法的具体实现取决于对象的实际类型,而非其声明类型。父类引用可以指向不同的子类对象,使得相同方法调用产生不同的行为结果。通过运行时确定具体执行的方法,代码具有更好的扩展性和可维护性。
重载指在同一个类中可以有多个方法,这些方法名称相同但参数列表不同(参数数量或类型不同)。
编译器在编译阶段就能确定具体的方法。以下是一个重载示例,展示了多个同名方法,但参数个数或类型不同。重载的好处是简化接口设计,不需要为不同类型编写多个方法名。
java//全部源码见文档链接/***重载示例,同名方法,参数个数或类型不同。*编译器在编译时确定具体的调用方法。*/classCalculator{publicintadd(intnum1,intnum2){returnnum1+num2;}publicintadd(intnums){intsum=0;for(intnum:nums){sum+=num;}returnsum;}}
运行时多态,方法重写(Override)与转型(Casting):运行时多态是在程序运行时确定实际要执行的方法。
当子类继承父类并覆盖同名方法时,这称为重写。使用父类引用来声明子类对象时,子类会向上转型为父类类型。调用该对象的方法时,实际执行的是子类的方法,而不是父类的方法。
向上转型是指使用父类引用声明子类对象,使子类对象的实际类型变为父类。通过父类引用调用子类的方法,使代码更加通用,处理一组相关对象时无需知道它们的具体类型。
向下转型则是将父类引用转换为子类引用,这需要显式进行,并且在转换前需要使用instanceof关键字进行类型检查。
java//全部源码见文档链接/***重写示例,子类覆盖父类同名方法,体现多态。*子类向上转型为父类型,父类强制向下转型为子类型。*/classShape{voiddraw(){("Shape-draw");}voiddrawShape(){("Shape-drawShape");}}classCircleextsShape{@Overridevoiddraw(){("Circle-draw");}voiddrawCircle(){("Circle-drawCircle");}}classSquareextsShape{@Overridevoiddraw(){("Square-draw");}voiddrawSquare(){("Square-drawSquare");}}publicclassOverrideExample{publicstaticvoidmain(String[]args){//用父类引用声明子类对象,向上转型Shapeshape1=newCircle();Shapeshape2=newSquare();//子类有同名方法,动态绑定到子类,实质执行的是(),体现多态();//报错,编译时按声明类型检查,Shape类中没有drawCircle方法//();//执行父类方法,输出"Shape-drawShape"();if(shape2instanceofSquare){//向下转型,用子类重新声明,成为子类型了SquaremySquare=(Square)shape2;//输出"Square-draw"();//输出"Square-drawSquare"();//报错。若强转为父类型,则无法调用drawSquare方法//((Shape)mySquare).drawSquare();//继承父类,输出"Shape-drawShape"();}}}
多态三个必要条件:严格来说,多态需要具备以下三个条件。
继承:子类继承父类或实现接口。
重写:子类覆盖父类的方法。
父类声明子类:使用父类引用来声明子类对象。
重载不属于严格意义上的多态,因为重载在编译阶段就确定了。我们主要探讨运行时的多态,即针对某个类型的方法调用,实际执行的方法取决于运行时的对象,而不是声明时的类型。
java//父类classAnimal{voidmakeSound(){("Animalmakesasound");}}//子类继承并重写同名方法classDogextsAnimal{@OverridevoidmakeSound(){("Dogbarks");}}publicclassTest{publicstaticvoidmain(String[]args){//父类引用声明子类AnimalmyAnimal=newDog();//运行时对象为子类,故输出"Dogbarks"();}}
如何理解父类声明子类Parentchild=newChild();?
解释:用Parent类声明了一个child引用变量(变量存于栈中),并赋值为Child实例对象(对象存于堆中)。变量child的类型为Parent(向上转型),它的值是一个Child类型的实例对象。
加载执行顺序:编译时:JVM编译时检查类的关系和对应方法(包括重载),确定变量的类型并定位相关方法名称,生成字节码。运行时:
JVM加载Parent和Child类。
根据Parent和Child的大小分配堆内存。
初始化newChild()并返回对象引用。
分配栈内存给变量child。
将对象引用赋值给child。
总结:编译时根据引用类型(不是实例对象)确定方法的名称和参数(包括重载)。运行时如果子类覆盖了父类的方法,则调用子类(实例引用类型)的方法;如果没有覆盖,则执行父类(变量引用类型)的方法。
在面向对象设计中,“开闭原则”是非常重要的一条。即系统中的类应该对扩展开放,而对修改关闭。这样的代码更可维护和可扩展,同时更加简洁与清晰。
延续上面的例子,假设业务需要扩充更多子类,我们可以通过以下步骤来体现开闭原则:
新增子类:根据业务需求,新增符合现有类层次结构的子类,例如增加AnotherChild。
继承和重写:新的子类应该继承自适当的父类,并根据需要重写父类的方法或添加新的方法。
不需要修改现有的代码:遵循开闭原则,我们不修改现有的Parent和Child类的代码。
使用多态:通过父类引用来声明子类,例如Parentchild=newAnotherChild();,这样代码中现有的逻辑不需要改变。
编译时不变性:编译时确定方法调用的特性不改变,仍然根据引用类型来确定方法的名称和参数,子类随意增加,只要覆盖父类同名方法即可。
运行时多态性:运行时根据实际对象的类型来决定要执行的方法,这使得代码具有良好的可扩展性和可维护性。
java//定义一个通用Animal类classAnimal{voidmakeSound(){("Animalmakesasound");}}//定义Dog类,它是动物的子类classDogextsAnimal{@OverridevoidmakeSound(){("Dogbarks");}}//定义Cat类,它是动物的子类classCatextsAnimal{@OverridevoidmakeSound(){("Catmeows");}//Cat自有方法voidmeow(){("Catismeowing");}}//定义一个动物园类,管理不同的动物classZoo{//传入的是抽象父类或接口,方便扩展voidletAnimalMakeSound(Animalanimal){();}}publicclassAnimalExample{publicstaticvoidmain(String[]args){Zoozoo=newZoo();AnimalmyDog=newDog();//向上转型AnimalmyCat=newCat();//向上转型((Cat)myCat).meow();//向下强转,打印自有方法//通过多态性,动物园可以使用相同的方法处理不同种类的动物(myDog);//输出"Dogbarks"(myCat);//输出"Catmeows"}}
要增加新的动物(如鸟类,Bird),只需扩展Animal类,而无需修改现有Zoo类中的方法。
javaclassBirdextsAnimal{@OverridevoidmakeSound(){("Birdchirps");}}publicclassAnimalExample{publicstaticvoidmain(String[]args){Zoozoo=newZoo();AnimalmyDog=newDog();//向上转型AnimalmyCat=newCat();//向上转型AnimalmyBird=newBird();//向上转型//通过多态性,动物园可以使用相同的方法处理不同种类的动物(myDog);//输出"Dogbarks"(myCat);//输出"Catmeows"(myBird);//输出"Birdchirps"}}
这种设计:
允许新增Animal的子类,保持对扩展开放;
无需修改依赖Zoo的letAnimalMakeSound方法,实现对修改封闭。
我们的业务总在不停变化,如何使得代码底层不用大改,而表层又能跟随业务不停变动,这就显得十分重要。通过这种方式,我们在不修改现有代码的情况下,可以轻松地引入新的子类并扩展系统功能,同时保持现有代码的稳定性和可靠性。
不同语言因为语言特性的不同,在实现多态上也有不同。Go语言有接口,有struct,但没有继承和方法重载,实现多态与Java有所不同。Python和JavaScript作为动态语言,没有接口和显式类型声明,但由于其本身的灵活性,在实现多态上也跟Java有区别。C语言没有class和接口,struct也没有成员函数,可通过struct和函数指针来模拟多态。C++有class,在多态上跟Java有点像,但其支持多重继承,且显示声明为virtual的方法才支持动态绑定,其核心机制上与Java也有所不同。
虽然各语言实现多态各不相同,但总的概念是一致的,即通过多态达到“开闭原则”的设计目标。以下一些语言的例子,其他例子请从仓库查找源码。
在Go语言中,虽然没有传统意义上的类继承、父类声明子类和方法重载,但通过结构体(struct)和接口(interface)以及匿名组合等方式实现类似的功能。这样也能实现代码的组织和复用,同时保持了灵活性和简洁性。
gopackagemainimport("fmt")//定义一个Animal接口typeAnimalinterface{MakeSound()}//定义一个Dog类型typeDogstruct{}//实现Animal接口的MakeSound方法func(dDog)MakeSound(){("Dogbarks")}//定义一个Cat类型typeCatstruct{}//实现Animal接口的MakeSound方法func(cCat)MakeSound(){("Catmeows")}//Cat自有方法func(c*Cat)Meow(){("Catismeowing")}//定义一个Zoo类型,用于管理动物typeZoostruct{}//定义一个方法,让动物发出声音func(zZoo)LetAnimalMakeSound(aAnimal){()}funcmain(){zoo:=Zoo{}myDog:=Dog{}//接口断言varmyCatAnimal=Cat{}//类型断言,打印自有方法(myCat.(*Cat)).Meow()//使用多态性,通过接口类型处理不同的具体类型(myDog)//输出"Dogbarks"(myCat)//输出"Catmeows"}
当需要增加Bird类型时,直接增加即可。同样无需修改Zoo类里面的LetAnimalMakeSound方法。
gotypeBirdstruct{}//实现Animal接口的MakeSound方法func(bBird)MakeSound(){("Birdchirps")}funcmain(){zoo:=Zoo{}myDog:=Dog{}varmyCatAnimal=Cat{}(myCat.(*Cat)).Meow()myBird:=Bird{}//使用多态性,通过接口类型处理不同的具体类型(myDog)//输出"Dogbarks"(myCat)//输出"Catmeows"(myBird)//输出"Birdchirps"}
严格的多态概念,包括子类继承父类、方法重写以及父类声明子类等,这些特性在Go语言中无法实现。Go语言没有class概念,虽然它的struct可以包含方法,看起来像class,但实际上没有继承和重载的支持,它们本质上仍是结构体。
Go语言摒弃了传统面向对象语言中的class和继承概念,我们需要用新的视角来理解和实践面向对象编程在Go中的应用方式
JavaScript是一种动态弱类型的基于对象的语言,其一切皆是对象。它通过对象的原型链来实现面向对象编程。尽管JavaScript具有class和继承的能力,但由于缺少强类型系统,因此无法实现传统意义上的多态。
当然,JavaScript作为动态语言,具有天然的动态性优势。这使得它在灵活性和扩展性方面更具优势。
js//定义一个通用Animal类classAnimal{makeSound(){("Animalmakesasound");}}//定义Dog类,它是动物的子类classDogextsAnimal{makeSound(){("Dogbarks");}}//定义Cat类,它是动物的子类classCatextsAnimal{makeSound(){("Catmeows");}//Cat自有函数meow(){("Catismeowing",this);}}//定义一个动物园类,管理不同的动物classZoo{//JS没有严格类型,出原始数据类型外,其他均是Object//说出传入的对象只要有makeSound方法即可。letAnimalMakeSound(animal){();}}//测试代码constzoo=newZoo();//JS没有父类定义子类概念,直接声明即可,无需向上转型//通过instanceof类型判断时可得到子类和父类类型constmyDog=newDog();constmyCat=newCat();//直接调用自有函数();//可以动态给对象设置函数并绑定对象=(myDog);();//动物园可以使用相同的方法处理不同种类的动物//当需要增加其他动物时,直接建立新的类继承Animal,而无需修改Zoo。(myDog);//输出"Dogbarks"(myCat);//输出"Catmeows"
可以看出JS要实现Java意义的多态是做不到的,但JavaScript更加灵活方便,声明对象无需类型,还可以动态添加函数和绑定对象。
py定义Dog类,继承AnimalclassDog(Animal):name="Dog"defmake_sound(self):print("Dogbarks")Cat自有方法defmeow(self):print(+"ismeowing")定义管理类classZoo:测试代码if__name__=="__main__":zoo=Zoo()直接调用自有方法my_()动物园可以使用相同的方法处理不同种类的动物_animal_make_sound(my_dog)输出"Catmeows"_animal_make_sound(my_bird)#输出"Birdchirps"
Python是一种动态语言,它使用self参数来引用实例,无需像其他语言那样使用new关键字来实例化对象。Python没有严格的接口概念,不需要像其他语言那样显示声明对象的接口。Python通过继承和方法重写来实现多态概念,但不支持传统意义上的父类声明子类和方法重载。
因此,Python在多态性上的表现与JavaScript相似,都是基于动态语言特性,灵活而动态,通过继承和重写实现对象行为的多样性。
理解Java多态的实例可以帮助澄清其原理和执行过程。以下是一个简单而详尽的例子,帮助你全面理解Java中多态的工作机制。
多态包括编译时多态和运行时多态。编译时多态,即静态绑定,通常通过方法重载实现。运行时多态则是在代码运行时确定具体调用的方法。
从Java的角度看,严格意义上的多态需要满足三个条件:继承、方法覆盖和父类引用子类对象。Java完全符合这些要求,实现了严格意义上的多态。
Go语言、Python和JavaScript不完全符合严格意义上的多态,但具备多态特性,能够达成动态确定实际要执行的方法,从而使代码更加灵活、易于维护和扩展。
_
版权声明:本站所有作品(图文、音视频)均由用户自行上传分享,仅供网友学习交流,不声明或保证其内容的正确性,如发现本站有涉嫌抄袭侵权/违法违规的内容。请举报,一经查实,本站将立刻删除。