转载本文请联系小菜良记公众号。”今天是周五,照例来到公司,在办公桌前坐下,打开电脑,“又是搬砖的一天。咦,这些代码是谁写的,怎么会出现在我的代码里,还在pendingsubmission中,我记得我没写过,兴致勃勃的看了看:这不是Polymorphism,谁写的测试我的电脑,我不禁有些奇怪,“看看这个输出?”一个声音从身后传来,因为我正在考虑输出,没有在意声音的来源,所以我继续说下去阅读看代码,得出结论:/*polygon()beforecal()square.cal(),border=2polygon()aftercal()square.square(),border=4*/我想:这个是吗?至少也是个名字Java开发工程师呢?虽然平时搬砖,但基本功还是有的,不禁有些小得意~“这就是你吗?”你回答?看来你不太行啊”的声音突然又响起,这回我不淡定了,尼玛!我心里也想过这个答案,好吧,谁看得出来,这话让人想表演一套“你是谁?”有点疑惑的转过头,怎么没人啊?我的疑惑不能容忍,“小菜,醒醒,你干嘛睡着了?”小时?”上班时间,睡着了?我睁开眼睛看了看周围的环境,原来是梦,我松了口气,看到科室长站在我面前,睡着了上班时间,是身体不舒服还是怎么的?昨天写了一堆bug还没改,今天提交了一些乱七八糟的东西我觉得你这个月的表现不是你想要的,根据你的表现,我会开始为部门着想。你的解释!”我还没来得及说出这句话,我想带着心中的花带你回家,在那深夜的酒吧里,不管是真是假,尽情挥舞吧忘记你喜欢的人,你是最迷人的因果报应,你知道吗,闹钟响了,我猛地站起来,后背微湿,额头微微出汗,一看手机,周六八点三十分,原来是梦!奇怪,我怎么会做这么奇怪的梦呢?太可怕了。然后就想到了梦里的那部分代码。我的结果错了吗?我凭着记忆在电脑上又敲了一遍,运行结果如下:/*polygon()beforecal()square.cal(),border=0polygon()aftercal()square.square(),border=4*/square.cal(),border的结果是0而不是2,我现在连多态都不知道吗?电脑和手机前的你,不知道自己是否答对了!不管你有没有,让我们和小菜一起回顾一下多态吧!有朋友疑惑点可能不止square.cal(),border的结果是0,还有疑惑为什么不是square.square(),border=4先输出。然后我们有疑惑,成立!多态性“在面向对象的编程语言中,多态性是继数据抽象和继承之后的第三个基本特征。”多态性不仅可以提高代码的组织结构和可扩展性的可读性,而且能够创建可扩展的程序。多态的作用是消除类型之间的耦合关系。1、向上转型基于里氏代换原则:凡是能出现基类的地方,就一定会出现子类。一个对象既可以用作它自己的类型,也可以用作它的基类型。这种将对对象的引用视为对其基类型的引用的方式称为-向上转型。因为父类在子类之上,子类需要引用父类,所以叫向上转型。publicclassAnimal{voideat(){System.out.println("Animaleat()");}}classMonkeyextendsAnimal{voideat(){System.out.println("Monkeyeat()");}}classtest{publicstaticvoidstart(Animalanimal){animal.eat();}publicstaticvoidmain(String[]args){Monkeymonkey=newMonkey();start(monkey);}}/*OUTPUT:Monkeyeat()*/上面测试类中的start()方法接收到对Animal,自然也可以接收来自Animal的派生类。调用eat()方法时,自然会使用Monkey中定义的eat()方法,不需要任何类型转换。因为从Monkey到Animal的向上转型只能减少接口,不会比Animal的接口少。打个不恰当的比方:你父亲的财产会继承给你,但你的财产还是你的。一般来说,你的财产不会比你父亲的少。忘了对象类型在test.start()方法中,传入的定义是对Animal的引用,但是传给Monkey。好像忘记了Monkey的对象类型,那为什么不直接在测试类中定义方法为voidstart(Monkeymonkey),这样岂不是更直观。直觉可能是它的优势,但它会带来其他问题:Animal有不止一个Monkey导出类。这时候有猪了,是不是还要再定义一个方法为voidstart(Monkeymonkey)重载呢?好用啊,小子,就是太麻烦了。懒惰是开发人员的天性。因此,这种方式存在多态性。2.揭示优势“方法调用”分为静态绑定和动态绑定。什么是绑定:将方法调用与方法体相关联称为绑定。静态绑定:也称为“早期绑定”。绑定是在程序执行之前进行的。平时我们听到“static”的时候,不免会想到static关键字。被static关键字修饰的变量成为静态变量,在程序执行前被初始化。早期绑定是面向过程语言中的默认绑定方法。比如C语言只有一个方法调用,就是早期绑定。《思维导论:》publicstaticvoidstart(Animalanimal){animal.eat();}在start()方法中,传入Animal对象引用,如果有多个Animal导出类,当eat()方法为执行?知道调用哪个方法。它不能通过早期绑定来实现。因此,后期绑定。动态绑定:也称为后期绑定。它是根据程序运行时的对象类型进行绑定的,所以也可以称为运行时绑定。而Java是基于自身的后期绑定机制,使其能够在运行时判断对象的类型,从而调用正确的方法。”总结:“Java中除了static和final修饰的方法外,都属于后期绑定,合理正确。显然,通过动态绑定实现多态是合理的。这样我们只需要在开发接口的时候传入基类的引用,这样这些代码就可以对基类的所有派生类正确运行。其中Monkey、Pig、Dog都是Animal的派生类。Animalanimal=newMonkey()看似赋值不正确,但是通过继承,Monkey是Animal的一种。如果我们调用的是animal.eat()方法,不知道有多少Stateful的朋友经常会误以为调用了Animal的eat()方法,最后却调用了Monkey自己的eat()方法。作为一个基类,Animal的作用是为派生类建立一个公共接口。所有继承自Animal的派生类都可以有自己独特的实现行为。可扩展性借助多态机制,我们可以根据需要向系统添加任意数量的新类型,而无需重载voidstart(Animalanimal)方法。在设计良好的OOP程序中,大多数或所有方法都将遵循start()方法的模型,并且只使用基类接口。这样的程序是“可扩展的”。继承新的数据类型以增加一些功能,操作基类接口的方法可以应用到新类中而不需要做任何改变。失败的?先回顾一下权限修饰符:当前类的作用域使用一个包子类其他包public√√√√protected√√√×default√√××private√××ד私有方法导致的失败”:回顾之后,我们再来看一组代码:}}classPrivateOverrideextendsPrivateScope{privatevoidf(){System.out.println("PrivateOverridef()");}}/*OUTPUTPrivateScopef()*/是不是有点奇怪为什么此时调用的f()定义在基类中,和上面不同的是,通过动态绑定,从而调用导入的类PrivateOverride中定义的f()不知道大家有没有注意到基类中f()方法的修改是“私有的”。没错,就是这个问题。PrivateOverride中定义的f()方法是一个全新的方法。因为private,对子类是不可见的,自然不能重载。结论:只能覆盖非私有修饰的方法。我们通过Idea写代码的时候,可以在重写的方法头上标注@Override注解。如果不是重写的方法,打上@Override注解会报错:这也是可以的很好的提醒,我们不是重写方法,而是全新的方法。《域带来的失败》:小伙伴们看到这里,会开始认为一切(私有修饰除外)都可以多态发生。然而,现实并不是这样的,“只有普通的方法调用才能多态”。对多态的误解就在这里。我们再看下面这组代码:(String[]args){Supersup=newSon();System.out.println("sup.field:"+sup.field+"sup.getField():"+sup.getField());Sonson=newSon();System.out.println("son.field:"+son.field+"son.getField:"+son.getField()+"son.getSupField:"+son.getSuperField());}}/*OUTPUTsup.field:0sup.getField():1son.field:1son.getField:1son.getSupField:0*/从上面的代码可以看出,sup.field输出的值并不是在Son对象中定义的,而是在Son对象中定义的超本身。这与我们所知道的多态性有点冲突。事实上,当Super对象转化为Son引用时,任何域访问操作都会被编译器解析,所以它不是多态的。在这个例子中,为Super.field和Son.field分配了不同的存储空间,而Son类是从Super类派生出来的,所以Son实际上包含了两个字段,叫做fields:“自身+Super”。虽然这种问题看起来很头疼,但是在我们的开发规范中,通常都会将所有域都设置为private,这样就不能直接访问了,只能通过调用方法访问。《静态导致的失败》:看到这里,朋友们应该对多态有了一个大概的了解,但是不要掉以轻心,还有一种情况会导致失败,《就是如果一个方法是静态的,那么它的行为就是不是多态的。"老规矩,我们来看这组代码:);}}classStaticTest{publicstaticvoidmain(String[]args){StaticSupersup=newStaticSon();sup.staticTest();}}/*OUTPUTStaticSuperstaticTest()*/"静态方法与类关联,而不是与对象关联"3.构造函数和多态性首先我们要明白构造函数是不具有多态性的,因为构造函数其实是一个静态方法,只是静态声明是隐式的,让我们回到开头神秘的代码:输出结果为:/*polygon()beforecal()square.cal(),border=0polygon()aftercal()square.square(),border=4*/我们可以看到首先输出的是基类中构造函数的方法polygon.这是因为在构造t的过程中总是调用基类构造函数他派生类,并链接继承层次结构,以便调用每个基类构造函数。因为构造函数有一个特殊的任务:检查对象能否正确构造。派生类只能访问自己的成员,不能访问基类的成员(基类成员通常是私有的)。只有基类的构造函数才有权初始化自己的元素。因此,必须调用所有的构造函数,否则无法正确构造出完整的对象。步骤如下:调用基类的构造函数,这一步会不断递归,先构造这个层级的根,再构造下一层导出类,...,直到底层导出类依次调用成员ofdeclaration初始化方法调用导出类构造函数的主体。这不是一个特别恰当的比喻:你的外表先有你的父亲,你的父亲先有你的祖父吗?这是在构造函数内部逐渐链接起来的方式多态行为有没有想过如果在构造函数中调用被构造对象的动态绑定方法会发生什么?动态绑定调用是在运行时确定的,因为对象没有办法知道它是属于该方法所在的类还是该类的派生类。如果要在构造函数中调用动态绑定方法,则需要使用该方法的重写定义。但是,由于在对象完全构造之前调用重写方法,这可能会导致难以发现的隐藏错误。问题索引:一个动态绑定的方法调用会深入到继承层次中去,它可以调动派生类中的方法,如果我们在构造函数内部这样做,那么可能会调用一个方法,而这个方法所操作的成员可能没有初始化,肯定会酿成大祸。敏感的朋友,有没有想到一开始的代码:输出是:/*polygon()beforecal()square.cal(),border=0polygon()aftercal()square.square(),border=4*/当我们初始化方形对象时,我们会先初始化多边形对象。多边形构造函数中有一个cal()方法。此时使用动态绑定机制调用了square的cal(),但是这个时候变量border还没有初始化,int类型的默认值为0,所以有输出ofsquare.cal(),border=0。看到这里小伙伴们是不是有种隔着云雾见蓝天的感觉呢!这组代码初始化的实际过程是:在其他事情发生之前,将分配给对象的存储空间初始化为二进制零调用基类,在构造构造函数时,会调用重写的cal()方法。由于第1步,border的值为0。按照声明的顺序调用成员的初始化方法,调用派生类的构造函数体。~最后复习多态还好是一场梦,没有人发现我的菜。不知道电脑和手机前的你是不是和小菜一样,如果是,赶紧和小菜一起复习一下,免得让别人发现你不是多态!
