目录 1、 从吃烤山药重新认识多态 2、 多态前提条件【重点】 3、 多态的体现 4、 多态动态绑定与静态绑定 5、 多态特性的虚方法(virtual) 7、 向上转型 8、 向下转型 9、 向上向下转型再次分析【加餐不加价】 10、 多态与构造器之间的微妙 11、 多态的优点 12、 分析开篇的九个问题 13、 最后我们一起来正式分析那九个题 @ 我不想知道各位理解java多态没有烤山药的存在,java香不香的问题了,我不要你们认为,我只要我觉得 (感觉要被打....) 在博主认为多态绝对是面向对象的第三大特性中让很多小白同学以及初学者难以跨越的鸿沟,因为多态有很多细节性的知识,不花点时间,还真不好理解多态。这么说吧,如果你觉得你已经完全理解了多态,你不妨做做下面的程序,如果你能全都答对,那没问题了,多态对你来说真的不是问题!如果在第四个就趴下了,那可以看看这篇文章,或许对你有所帮助,可能会让你重新见识到多态的魅力。 package Polymorphic; //爷爷类 class Ye { public String show(Sun obj) { return ("Ye and Sun"); } public String show(Ye obj) { return ("Ye and Ye"); } } //爸爸类 class Fu extends Ye { public String show(Fu obj) { return ("Fu and Fu"); } public String show(Ye obj) { return ("Fu and Ye"); } } //儿子类 class Zi extends Fu { } //孙子类 class Sun extends Fu { } public class PolymorphicTest { public static void main(String[] args) { Ye y = new Ye(); Ye y2 = new Fu(); //向上 Fu f = new Fu(); Zi z = new Zi(); Sun s = new Sun(); System.out.println("第一题 " + y.show(f)); System.out.println("第二题 " + y.show(z)); System.out.println("第三题 " + y.show(s)); System.out.println("第四题 " + y2.show(f)); //到这里挂了??? System.out.println("第五题 " + y2.show(z)); System.out.println("第六题 " + y2.show(s)); System.out.println("第七题 " + f.show(f)); System.out.println("第八题 " + f.show(z)); System.out.println("第九题 " + f.show(s)); } } 先把答案记在小本本上吧,再对照下面结果看看 第一题 Ye and Ye 第二题 Ye and Ye 第三题 Ye and Sun 第四题 Fu and Ye 第五题 Fu and Ye 第六题 Ye and Sun 第七题 Fu and Fu 第八题 Fu and Fu 第九题 Ye and Sun 如果你对上面的结果很意外,或者不解,那么恭喜你,你又能学到新知识了,成功的向架构师前进了一步!好了,让我们一起重新见识见识多态的魅力吧! 1、 从吃烤山药重新认识多态 最近不是正火着吃烤山药么,学习就要走有趣化路线,毕竟兴趣永远最好的老师,咋们放开点,怎么有趣怎么来。 小明妈妈的情绪非常不稳定,心情好的时候巴不得给小明花一个亿,,心情不好的时候巴不得把小明打成麻瓜,可是小明永远不知道妈妈的情绪变化。这不,今天一位老大爷在卖烤山药,边烤还边跳激光雨,嗨得不行,小明特别喜欢激光雨,马上就忍不住了,心里默默想着,刚烤的山药它不香嘛,激光雨烤的山药它不香嘛。于是忍不住对妈妈说:“妈妈,我想吃烤山药”,这个时候,来了,来了,他来了,它真的来了....你激动个锤子啊......是代码来了: package Polymorphic; class Matcher{ public void matcherSpeak(){ System.out.println("想吃烤山药?"); } } class HappyMother extends Matcher { public void matcherSpeak(){ System.out.println("开心的妈妈说:吃,吃大块的,一火车够吗"); } } class SadMother extends Matcher { public void matcherSpeak(){ System.out.println("不开心的妈妈说:吃你个憨皮,看我回家扎不扎你就完事了"); } } class VeryHappyMother extends Matcher { public void matcherSpeak(){ System.out.println("异常开心的妈妈说:买买买,烤山药咱全买了,顺便把大爷也买回家,天天给你表演激光雨(大爷懵逼中)"); } } public class UnderstandPolymorphic{ public static void main(String[] args) { Matcher m = new HappyMother(); m.matcherSpeak(); m = new SadMother(); m.matcherSpeak(); m = new VeryHappyMother(); m.matcherSpeak(); } } 运行结果: 开心的妈妈说:吃,吃大块的,一火车够吗 不开心的妈妈说:吃你个憨皮,看我回家扎不扎你就完事了 异常开心的妈妈说:买买买,烤山药咱全买了,顺便把大爷也买回家,天天给你表演激光雨(大爷懵逼中) 妈妈听到小明想吃烤山药这同一行为,表现出不同的表现形式,这就是多态。多态专业定义则是:程序中定义的引用变量所指向的具体类型和通过该引用变量发出的方法调用在编程时并不确定,而是在程序运行期间才确定,这种情况叫做多态没错是没错就是脑壳有点大,所以我选择简单点定义多态: 多态指同一行为,具有多个不同表现形式。为何会有如此微妙的变化呢,那我们就必须了解进行多态的前提了。 2、 多态前提条件【重点】 如果多态不能满足以下三个前提条件,那还玩犊子的多态【构不成多态,缺一不可】 继承或者实现【二选一】 方法的重写【意义体现:不重写,无意义】 子类对父类中某些方法进行重新定义,在调用这些方法时就会调用子类的方法。 父类引用指向子类对象(也可以说向上转型)【体现在格式上】 回过头来看烤山药例子,确实都有继承,同样都重写了motherSpeak()方法,最关键的代码则是 Matcher m = new HappyMother(); 也就是所谓的 父类引用指向子类对象,这其实就是向上转型!对向上转型概念不清晰没事,下面会详细讲解。 3、 多态的体现 多态体现的格式: 父类/父接口类型 变量名 = new 子类对象; 变量名.方法名(); 当使用多态方式调用方法时,首先检查父类中是否有该方法,如果没有,则编译错误 ,如果有,执行的是子类重写后的方法,也就是向上转型时, 子类单独定义的方法丢失问题。编译报错。 代码如下: package Demo; class Matcher{ public void matcherSpeak(){//=========================父类matcherSpeak()方法 System.out.println("吃烤山药?"); } } class HappyMother extends Matcher { public void matcherSpeak(){//=========================子类matcherSpeak()方法 System.out.println("开心的妈妈说:吃,吃大块的,一蛇皮袋够吗"); } public void fatherSpeak(){//=========================子类独有的fatherSpeak()方法 System.out.println("开心的妈妈说:吃,吃大块的,一麻袋够吗"); } } public class Test { public static void main(String[] args) { Matcher m=new HappyMother(); m.matcherSpeak(); m.fatherSpeak(); //编译失败,无法解析fatherSpeak方法 } } 分析如下: 在这里插入图片描述 当然这个例子只是入门级的,接下来看个有点水平的例子 package Demo; class Matcher{ public void matcherSpeak(){ System.out.println("想吃烤山药?"); } } class HappyMother extends Matcher { public void matcherSpeak(){ System.out.println("开心的妈妈说:吃,吃大块的,一火车够吗"); } } class SadMother extends HappyMother{ public void tt(){ System.out.println("ttttttt"); } } public class Test { public static void main(String[] args) { Matcher mm=new SadMother(); mm.matcherSpeak(); } 运行结果:开心的妈妈说:吃,吃大块的,一火车够吗 } 有了第一个基础这个相信不难理解,接着看 package Demo; class Matcher{ public void matcherSpeak(){ System.out.println("想吃烤山药?"); } } class HappyMother extends Matcher { } class SadMother extends HappyMother{ public void tt(){ System.out.println("ttttttt"); } } public class Test { public static void main(String[] args) { Matcher mm=new SadMother(); mm.matcherSpeak(); } 运行结果:想吃烤山药? } 到这里,再来回味下这句话: 当使用多态方式调用方法时,首先检查父类中是否有该方法,如果没有,则编译错误 ,如果有,执行的是子类重写后的方法 你可能会说子类中都没有这些个方法啊,何来执行子类重写后的方法一说?它好像是去父类中找该方法了。事实上,子类中是有这些方法的,这个方法继承自父类,只不过没有覆盖该方法,所以没有在子类中明确写出来而已,看起来像是调用了父类中的方法,实际上调用的还是子类中的。同学继承方面的知识该补补了,可以参考下面这篇【java基础】java继承从“我爸是李刚”讲起 4、 多态动态绑定与静态绑定 讲之前博主先来谈谈“绑定”的概念: 绑定指的是一个方法的调用与方法所在的类(方法主体)关联起来,大致可以理解为一个方法调用另一个方法。对java来说,绑定分为静态绑定和动态绑定;或者分别叫做前期绑定和后期绑定。 4、1.静态绑定(前期绑定) 在程序执行前方法已经被绑定,==针对java静态绑定简单的可以理解为程序编译期的绑定==;java当中的方法只有final,static,private(不会被继承) 和构造方法是前期绑定【当然可能不止】 4、2.动态绑定(后期绑定) 后期绑定:在运行时根据具体对象的类型进行绑定。 若一种语言实现了后期绑定,同时必须提供一些机制,可在运行期间判断对象的类型,并分别调用适当的方法。也就是说,编译器此时依然不知道对象的类型,但方法调用机制能自己去调查,找到正确的方法主体。不同的语言对后期绑定的实现方法是有所区别的。但我们至少可以这样认为:它们都要在对象中安插某些特殊类型的信息。==简明的说动态绑定就是指编译器在编译阶段不知道要调用哪个方法,运行期才能确定== 4、3.静态、动态绑定本质区别 1、静态绑定是发生在编译阶段;而动态绑定是在运行阶段; 2、静态绑定使用的是类信息,而动态绑定使用的是对象信息 3、重载方法(overloaded methods)使用的是静态绑定,而重写方法(overridden methods)使用的是动态绑定 4、4.静态、动态绑定在程序中运行区别 这个静态绑定例子以static方法为例,代码程序如下: package demoee; class Father5{ public void StaticMethod(){ System.out.println("粑粑:我是父类粑粑静态方法"); } } class Son5 extends Father5{ public void StaticMethod(){ System.out.println("熊孩子:我是子类熊孩砸静态方法"); } } public class demooo { public static void main(String[] args) { Father5 fat=new Father5(); Father5 son=new Son5(); //特别注意这里是向上转型 也就是多态! fat.StaticMethod();//同时调用StaticMethod方法! son.StaticMethod(); } } 运行结果 粑粑:我是父类粑粑静态方法 熊孩子:我是子类熊孩砸静态方法 根据上面的运行结果,我们也很好理解!子类重写了父类的一个叫做StaticMethod()的方法,由于是动态绑定,因此最后执行的是子类重写后的StaticMethod()方法。 嗯哼?为了更好的理解静态、动态绑定在程序中运行区别,我们还是得看看下面这个程序: class Father5{ public static void StaticMethod(){ System.out.println("粑粑:我是父类粑粑静态方法"); } } class Son5 extends Father5{ public static void StaticMethod(){ System.out.println("熊孩子:我是子类熊孩砸静态方法"); } } public class demooo { public static void main(String[] args) { Father5 fat=new Father5(); Father5 son=new Son5(); //特别注意这里是向上转型 也就是多态! fat.StaticMethod();//同时调用StaticMethod方法! son.StaticMethod(); } } 千万注意哦,这个程序与第一个程序唯一不同之处就在于这个程序父类和子类的方法都是static的! 运行结果: 粑粑:我是父类粑粑静态方法 粑粑:我是父类粑粑静态方法 从运行结果来看,我们可以很清楚的知道,子类静态方法语法上是做到了重写的作用,但实际上并没有做到真正意义上重写作用!只因为该方法是静态绑定! OK,get到了咩?如果get到了请点个赞呗,谢谢你~ 5、 多态特性的虚方法(virtual) 虚方法出现在Java的多态特性中。 父类与子类之间的多态性,对父类的函数进行重新定义。如果在子类中定义某方法与其父类有相同的名称和参数,我们说该方法被重写 (Overriding)。在Java中,子类可继承父类中的方法,而不需要重新编写相同的方法。但有时子类并不想原封不动地继承父类的方法,而是想作一定的修改,这就需要采用方法的重写。方法重写又称方法覆盖。 当设计类时,被重写的方法的行为怎样影响多态性。方法的重写使得子类能够重写父类的方法。当子类对象调用重写的方法时,调用的是子类的方法,而不是父类中被重写的方法。 因此简单明了的理解Java虚方法方式你可以理解为java里所有父类中被重写的方法都是虚方法(virtual)差不多的意思就是该方法不会被子类使用到,使用到的都是子类中重写父类的方法,子类中的重写方法代替了它,因此也就有种名存实亡的感觉! 在JVM字节码执行引擎中,方法调用会使用invokevirtual字节码指令来调用所有的虚方法。 小白童鞋千万需要注意虚方法和抽象方法并不是同一个概念! # 6、 重载属于多态吗? 纵观重载与重写,重写是多态的特征体现无疑了!但是对于重载是不是多态的体现网上却议论纷纷! 多态是基于对抽象方法的覆盖来实现的,用统一的对外接口来完成不同的功能。重载也是用统一的对外接口来完成不同的功能。那么两者有什么区别呢? 重载 重载是指允许存在多个同名方法,而这些方法的参数不同。重载的实现是:编译器根据方法不同的参数表,对同名方法的名称做修饰。对于编译器而言,这些同名方法就成了不同的方法。它们的调用地址在编译期就绑定了。 多态 多态是指子类重新定义父类的虚方法(virtual,abstract)。当子类重新定义了父类的虚方法后,父类根据赋给它的不同的子类,动态调用属于子类的该方法,这样的方法调用在编译期间是无法确定的。 不难看出,两者的区别在于编译器何时去寻找所要调用的具体方法,对于重载而言,在方法调用之前,编译器就已经确定了所要调用的方法,这称为“早绑定”或“静态绑定”;而对于多态,只有等到方法调用的那一刻,编译器才会确定所要调用的具体方法,这称为“晚绑定”或“动态绑定”。 所以,你可以大可认为重载不属于多态,多态是对父类虚函数的重定义,不改变原虚函数的参数列表。重载是函数名相同,但参数列表不同。 实际上这种问题没有严格的答案,就连教材书上都没提及。严格来说或狭义来讲,重载算多态还是有点牵强,传统的多态就是指父类和子类关系,但实际开发中都是理解重载是多态。这就是一个概念 你子类拥有你很多隐式父类的功能 那么你当然能扮演它们之中的某一个角色。 总的来说,在博主认为,重载是不是多态这个问题以及不重要了,首当其冲的重要任务我觉得还是好好保护头发,然后就是养生了.... 7、 向上转型 向上转型:多态本身是子类类型向父类类型向上转换的过程,其中,这个过程是默认的。你可以把这个过程理解为基本类型的小类型转大类型自动转换,不需要强制转换。 当父类引用指向一个子类对象时,便是向上转型。 向上转型格式: 父类类型 变量名 = new 子类类型(); 如:Father f= new Son(); 例子的话,烤山药的例子就是一个典型的向上转型例子 8、 向下转型 向下转型:父类类型向子类类型向下转换的过程,这个过程是强制的。同样可以把这个过程理解为基本类型的自动转换,大类型转小类型需要强制转换。一个已经向上转型的子类对象,将父类引用转为子类引用,可以使用强制类型转换的格式,向下转使用格式: Father father = new Son(); 子类类型 变量名 = (子类类型) 父类变量名; 如:Son s =(Son) father; 不知道你们有没有发现,向下转型的前提是父类对象指向的是子类对象(也就是说,在向下转型之前,它得先向上转型),当然,向下转型还是有它的意义所在,下面就讲解向下转型的意义。 到这里,我们讲解一下为什么要向下转型?上面已经讲到过当使用多态方式调用方法时,首先检查父类中是否有该方法,如果没有,则编译错误。也就是说,不能调用子类拥有,而父类没有的方法。编译都错误,更别说运行了。这也是多态给我们带来的一点"小麻烦"。所以,想要调用子类特有的方法,必须做向下转型。 package Demo; class Matcher{ public void eat(){ System.out.println("想吃烤山药?"); } } class XiongHaiZi extends Matcher { public void eat(){ System.out.println("妈妈,我想吃烤山药"); } public void eatSuLi(){//============================子类特有的eatSuLi方法 System.out.println("麻麻,我想吃酥梨,要吃麻瓜那么大的酥梨"); } } public class Test { public static void main(String[] args) { Matcher m = new XiongHaiZi();//向上转型 XiongHaiZi x = (XiongHaiZi)m;//向下转型 x.eatSuLi();//执行子类特有方法 } 运行结果:麻麻,我想吃酥梨,要吃麻瓜那么大的酥梨 } 好了向下转型就讲到这里...等等,你真的以为就讲完了?肯定不行喽,向下转型还有一个要说的知识,讲之前先来看个程序先 package Demo; class Matcher{ public void eat(){ System.out.println("想吃烤山猪?"); } } class Boy extends Matcher { public void eatKaoYang(){ System.out.println("妈妈,我想吃烤山猪"); } } class Girl extends Matcher { public void eatKaoYang(){ System.out.println("妈妈,我想吃烤山猪2333"); } } public class Test { public static void main(String[] args) { Matcher g = new Girl();//向上转型编译通过 Boy x = (Boy)g;//向下转型 x.eatKaoYang();//编译通过,但运行报ClassCastException } 运行结果: 运行报ClassCastException } 这段代码可以通过编译,但是运行时,却报出了 ClassCastException ,类型转换异常!这是因为,明明创建了Girl类型对象,运行时,当然不能转换成Boy对象的。这两个类型并没有任何继承关系,不符合类型转换的定义。 为了避免ClassCastException的发生,Java提供了 instanceof 关键字,给引用变量做类型的校验。 8、1. instanceof的使用 instanceof 的格式: 变量名 instanceof 数据类型 instanceof 的使用 如果变量属于该数据类型,返回true。 如果变量不属于该数据类型,返回false。 所以,转换前,我们最好使用instanceof 先做一个判断,代码如下: package Demo; class Matcher{ public void eat(){ System.out.println("想吃烤山药?"); } } class Boy extends Matcher { public void eatKaoYang(){ System.out.println("Boy:妈妈,我想吃烤羊"); } } class Girl extends Matcher { public void eatKaoYang(){ System.out.println("Girl:妈妈,我想吃烤全羊2333"); } } public class Test { public static void main(String[] args) { Matcher g = new Girl();//向上转型 if(g instanceof Girl){ Girl x = (Girl)g;//向下转型 x.eatKaoYang(); //=====================调用Girl的eatKaoYang()方法 }else if(g instanceof Boy){ //不执行 Boy x = (Boy)g;//向下转型 x.eatKaoYang(); //=====================调用Boy的eatKaoYang()方法 } } } 运行结果: Girl:妈妈,我想吃烤全羊2333 好了到这里,你get到了咩? 9、 向上向下转型再次分析【加餐不加价】 看完之后是不是还是不够清晰向上向下转型?多态转型问题其实并不复杂,只要记住一句话:父类引用指向子类对象。那什么叫父类引用指向子类对象?看下面例子吧 有两个类,Father 是父类,Son 类继承自 Father。 第 1 个例子: // f1 引用指向一个Son对象 Father f1 = new Son(); // 这就叫 upcasting (向上转型) // f1 还是指向 Son对象 Son s1 = (Son)f1; // 这就叫 downcasting (向下转型) 第 2 个例子: // f1现在指向father对象 Father f2 = new Father(); Son s2 = (Son)f2; // 出错,子类引用不能指向父类对象 你或许会问,第1个例子中:Son s1 = (Son)f1; 为什么是正确的呢。很简单因为 f1 指向一个子类对象,Father f1 = new Son(); 子类 s1 引用当然可以指向子类对象了。 而 f2 被传给了一个 Father 对象,Father f2 = new Father(); 子类 s2 引用不能指向父类对象。 10、 多态与构造器之间的微妙 直接上代码: package Polymorphic; class EatKaoShanYao { EatKaoShanYao () { System.out.println("吃烤山药之前..."); eat(); System.out.println("吃烤山药之后(熊孩子懵逼中)...."); } public void eat() { System.out.println("7岁半就喜欢吃烤山药"); } } public class KaoShanYao extends EatKaoShanYao { private String Weight = "110斤"; public KaoShanYao(String Weight) { this.Weight = Weight; System.out.println("熊孩子的体重:" + this.Weight); } public void eat() { // 子类覆盖父类方法 System.out.println("熊孩子吃烤山药之前的体重是:" + this.Weight); } //Main方法 public static void main(String[] args) { EatKaoShanYaok = new KaoShanYao("250斤"); } } 童鞋们可以试想一下运行结果,再看下面的输出结果 运行结果: 吃烤山药之前... 熊孩子吃烤山药之前的体重是:null 吃烤山药之后(熊孩子懵逼中).... 熊孩子的体重:250斤 是不是很疑惑?结果为啥是这样?你看,熊孩子又懵逼了,Why? 原因其实很简单,因为在创建子类对象时,会先去调用父类的构造器,而父类构造器中又调用了被子类覆盖的多态方法,由于父类并不清楚子类对象中的属性值是什么(先初始化父类的时候还没开始初始化子类),于是把String类型的属性暂时初始化为默认值null,然后再调用子类的构造器(这个时子类构造器已经初始Weight属性,所以子类构造器知道熊孩子的体重Weight是250)。 如果有什么不理解的可以及时告诉我,楼主一直都在,还有如果楼主哪里写错了或者理解错了,请及时告诉我,一定要告诉我!!!