相關學習推薦:java基礎教學
今天,跟往常一樣踩點來到了公司。坐到自己的工位上打開電腦,"又是搬磚的一天"
。想歸想,還是"熟練"
的打開了 Idea,看了下今天的需求,便敲起了程式碼。咦,這些程式碼是誰寫的,怎麼出現在我的程式碼裡面,而且還是待提交狀態,我記得我沒寫過呀,饒有興趣的看了看:
這不是多態嗎,誰在我電腦寫的測試,不禁一陣奇怪。
"你看看這會輸出什麼結果?"
一陣聲音從身後傳來,因為在思考輸出結果,也沒在意聲音的來源,繼續看了看程式碼,便下結論:
polygon() before cal() square.cal(), border = 2 polygon() after cal() square.square(), border = 4复制代码
心裡想:就這?起碼也是名 Java 開發工程師好嗎,雖然平常搬搬磚,一些基本功還是有的。不禁有點得意了~
"這就是你的答案嗎?看來你也不咋的"
聲音又突然響起,這次我不淡定了,尼瑪!這答案我也是在心裡想的好嗎,誰能看得到啊,而且說得話讓人那麼想施展一套阿威十八式。 "你是誰啊?"
帶著絲微疑惑和憤怒轉過頭了。怎麼沒人?容不得我疑惑半分,"小菜,醒醒,你怎麼上班時間就睡著了"
上班時間,睡著了?我睜開了眼,看了下周圍環境,原來是夢啊,舒了一口氣。望眼看到部門主管站在我面前,上班時間睡覺,你是身體不舒服還是咋樣?昨天寫了一堆 bug 沒改,今天又提交什麼亂七八糟的東西上去,我看你這個月的績效是不想要的,而且基於你的表現,我也要開始為部門考慮考慮了。
"我不是,我沒有,我也不知道怎麼就睡著了,你聽我解釋啊!"
這句話還沒來得及說出口,心裡的花我要帶你回家,在那深夜酒吧哪管它是真是假,請你盡情搖擺忘記鍾意的他,你是最迷人噶,你知道嗎
,鬧鐘響了起來,我一下子立起身子,背部微濕,額頂微汗,看了下手機,週六,8點30分,原來那是夢啊!
奇怪,怎麼會做那麼奇怪的夢,也太嚇人了。然後就想到了夢中的那部分程式碼,難道我的結果是錯的嗎?憑著記憶,在電腦上重新敲了出來,運行結果如下:
/* polygon() before cal() square.cal(), border = 0 polygon() after cal() square.square(), border = 4 */复制代码
square.cal(), border
的結果居然是 0,而不是2。難道我現在連多型都不會了嗎?電腦手機前的你,不知道是否得出正確答案了呢!不管有沒有,接下來就跟小菜一起來複習一下多型吧!
有些小夥伴疑惑的點可能不只square.cal(), border
的結果是0,也有為什麼不是 square.square(), border = 4
先輸出的疑惑。那我們就帶著疑惑,整起!
在物件導向的程式設計語言中,多型是繼資料抽象化和繼承之後的第三種基本特徵。
多態不僅能夠改善程式碼的組織結構和可讀性,還能夠創建可擴充的程式。多態的作用就是消除型別之間的耦合關係
。
根據里氏代換原則
:任何基底類別可以出現的地方,子類別一定可以出現。
物件既可以作為它自己本身的類型使用,也可以作為它的基底類型使用。而這種吧對某個物件的引用視為對其基底類型的引用的做法被稱為 - 向轉型
。因為父類在子類的上方,子類要引用父類,因此稱為 向上轉型
。
public class Animal { void eat() { System.out.println("Animal eat()"); } }class Monkey extends Animal { void eat() { System.out.println(" Monkey eat()"); } }class test { public static void start(Animal animal) { animal.eat(); } public static void main(String[] args) { Monkey monkey = new Monkey(); start(monkey); } }/* OUTPUT: Monkey eat() */复制代码
上述test
類別中的start()
方法接收一個Animal
的引用,自然也可以接收從Animal
的匯出類別。呼叫eat()
方法的時候,自然而然的使用到 Monkey
中定義的eat()
方法,而不需要做任何的型別轉換。因為從 Monkey
向上轉型到 Animal
只能減少接口,而不會比Animal
的接口更少。
打個不是特別恰當的比方:你父親的財產會繼承給你,而你的財產還是你的,總的來說,你的財產不會比你父親的少。
在 test.start()
方法中,定义传入的是 Animal
的引用,但是却传入Monkey
,这看起来似乎忘记了Monkey
的对象类型,那么为什么不直接把test
类中的方法定义为void start(Monkey monkey)
,这样看上去难道不会更直观吗。
直观也许是它的优点,但是就会带来其他问题:Animal
不止只有一个Monkey
的导出类,这个时候来了个pig
,那么是不是就要再定义个方法为void start(Monkey monkey)
,重载用得挺溜嘛小伙子,但是未免太麻烦了。懒惰才是开发人员的天性。
因此这样就有了多态
的产生
方法调用中分为 静态绑定
和动态绑定
。何为绑定:将一个方法调用同一个方法主体关联起来被称作绑定。
静态绑定
:又称为前期绑定。是在程序执行前进行把绑定。我们平时听到"静态"的时候,不难免想到static
关键字,被static
关键字修饰后的变量成为静态变量,这种变量就是在程序执行前初始化的。前期绑定
是面向过程语言中默认的绑定方式,例如 C 语言只有一种方法调用,那就是前期绑定。引出思考:
public static void start(Animal animal) { animal.eat(); }复制代码
在start()
方法中传入的是Animal
的对象引用,如果有多个Animal
的导出类,那么执行eat()
方法的时候如何知道调用哪个方法。如果通过前期绑定
那么是无法实现的。因此就有了后期绑定
。
动态绑定
:又称为后期绑定
。是在程序运行时根据对象类型进行绑定的,因此又可以称为运行时绑定
。而 Java 就是根据它自己的后期绑定机制,以便在运行时能够判断对象的类型,从而调用正确的方法。小结:
Java 中除了 static
和 final
修饰的方法之外,都是属于后期绑定
显然通过动态绑定
来实现多态
是合理的。这样子我们在开发接口的时候只需要传入 基类 的引用,从而这些代码对所有 基类 的 导出类 都可以正确的运行。
其中Monkey
、Pig
、Dog
皆是Animal
的导出类
Animal animal = new Monkey()
看上去不正确的赋值,但是上通过继承,Monkey
就是一种Animal
,如果我们调用animal.eat()
方法,不了解多态的小伙伴常常会误以为调用的是Animal
的eat()
方法,但是最终却是调用了Monkey
自己的eat()
方法。
Animal
作为基类,它的作用就是为导出类建立公用接口。所有从Animal
继承出去的导出类都可以有自己独特的实现行为。
有了多态机制,我们可以根据自己的需求对系统添加任意多的新类型,而不需要重载void start(Animal animal)
方法。
在一个设计良好的OOP程序中,大多数或者所有方法都会遵循start()
方法的模型,只与基类接口同行,这样的程序就是具有可扩展性的,我们可以通过从通用的基类继承出新的数据类型,从而添加一些功能,那些操纵基类接口的方法就不需要任何改动就可以应用于新类。
我们先来复习一下权限修饰符:
作用域 | 目前類別 | 用一個package | 子孫類別 | 其他package |
---|---|---|---|---|
public | √ | √ | #√ | √ |
protected | ||||
√ | ##√√× | default | √ | |
× | × | #private | ##√ |
私有方法带来的失灵:
复习完我们再来看一组代码:
public class PrivateScope { private void f() { System.out.println("PrivateScope f()"); } public static void main(String[] args) { PrivateScope p = new PrivateOverride(); p.f(); } }class PrivateOverride extends PrivateScope { private void f() { System.out.println("PrivateOverride f()"); } }/* OUTPUT PrivateScope f() */复制代码
是否感到有点奇怪,为什么这个时候调用的f()
是基类中定义的,而不像上面所述的那样,通过动态绑定
,从而调用导出类PrivateOverride
中定义的f()
。不知道心细的你是否发现,基类中f()
方法的修饰是private。没错,这就是问题所在,PrivateOverride
中定义的f()
方法是一个全新的方法,因为private
的缘故,对子类不可见,自然也不能被重载。
结论:
只有非 private
修饰的方法才可以被覆盖
我们通过 Idea 写代码的时候,重写的方法头上可以标注@Override
注解,如果不是重写的方法,标注@Override
注解就会报错:
这样也可以很好的提示我们非重写方法,而是全新的方法。
域带来的失灵:
当小伙伴看到这里,就会开始认为所有事物(除private
修饰)都可以多态地发生。然而现实却不是这样子的,只有普通的方法调用才可以是多态的。这边是多态的误区所在。
让我们再看看下面这组代码:
class Super { public int field = 0; public int getField() { return field; } }class Son extends Super { public int field = 1; public int getField() { return field; } public int getSuperField() { return super.field; } }class FieldTest { public static void main(String[] args) { Super sup = new Son(); System.out.println("sup.field:" + sup.field + " sup.getField():" + sup.getField()); Son son = new Son(); System.out.println("son.field:" + son.field + " son.getField:" + son.getField() + " son.getSupField:" + son.getSuperField()); } }/* OUTPUT sup.field:0 sup.getField():1 son.field:1 son.getField:1 son.getSupField:0 */复制代码
从上面代码中我们看到sup.field
输出的值不是 Son
对象中所定义的,而是Super
本身定义的。这与我们认识的多态有点冲突。
其实不然,当Super
对象转型为Son
引用时,任何域访问操作都将由编译器解析,因此不是多态的。在本例中,为Super.field
和Son.field
分配了不同的存储空间,而Son
类是从Super
类导出的,因此,Son
实际上是包含两个称为field
的域:它自己的+Super
的。
虽然这种问题看上去很令人头痛,但是我们开发规范中,通常会将所有的域都设置为 private,这样就不能直接访问它们,只能通过调用方法来访问。
static 带来的失灵:
看到这里,小伙伴们应该对多态有个大致的了解,但是不要掉以轻心哦,还有一种情况也是会出现失灵的,那就是如果某个方法是静态的,那么它的行为就不具有多态性。
老规矩,我们看下这组代码:
class StaticSuper { public static void staticTest() { System.out.println("StaticSuper staticTest()"); } }class StaticSon extends StaticSuper{ public static void staticTest() { System.out.println("StaticSon staticTest()"); } }class StaticTest { public static void main(String[] args) { StaticSuper sup = new StaticSon(); sup.staticTest(); } }/* OUTPUT StaticSuper staticTest() */复制代码
静态方法是与类相关联,而非与对象相关联
首先我们需要明白的是构造器不具有多态性,因为构造器实际上是static
方法,只不过该static
的声明是隐式的。
我们先回到开头的那段神秘代码:
其中输出结果是:
/* polygon() before cal() square.cal(), border = 0 polygon() after cal() square.square(), border = 4 */复制代码
我们可以看到先输出的是基类polygon
中构造器的方法。
这是因为基类的构造器总是在导出类的构造过程中被调用,而且是按照继承层次逐渐向上链接,以使每个基类的构造器都能得到调用。
因为构造器有一项特殊的任务:检查对象是否能正确的被构造。导出类只能访问它自己的成员,不能访问基类的成员(基类成员通常是private类型)。只有基类的构造器才具有权限来对自己的元素进行初始化。因此,必须令所有构造器都得到调用,否则就不可能正确构造完整对象。
步骤如下:
打个不是特别恰当的比方:你的出现是否先要有你父亲,你父亲的出现是否先要有你的爷爷,这就是逐渐向上链接的方式
有没有想过如果在一个构造器的内调用正在构造的对象的某个动态绑定方法,那么会发生什么情况呢? 动态绑定的调用是在运行时才决定的,因为对象无法知道它是属于方法所在的那个类还是那个类的导出类。如果要调用构造器内部的一个动态绑定方法,就要用到那个方法的被覆盖后的定义。然而因为被覆盖的方法在对象被完全构造之前就会被调用,这可能就会导致一些难于发现的隐藏错误。
问题引索:
一个动态绑定的方法调用会向外深入到继承层次结构内部,它可以调动导出类里的方法,如果我们是在构造器内部这样做,那么就可能会调用某个方法,而这个方法做操纵的成员可能还未进行初始化,这肯定就会招致灾难的。
敏感的小伙伴是不是想到了开头的那段代码:
输出结果是:
/* polygon() before cal() square.cal(), border = 0 polygon() after cal() square.square(), border = 4 */复制代码
我们在进行square
对象初始化的时候,会先进行polygon
对象的初始化,在polygon
构造器中有个cal()
方法,这个时候就采用了动态绑定机制,调用了square
的cal()
,但这个时候border
这个变量尚未进行初始化,int 类型的默认值为 0,因此就有了square.cal(), border = 0
的输出。看到这里,小伙伴们是不是有种拨开云雾见青天的感觉!
这组代码初始化的实际过程为:
cal()
方法,由于步骤1的缘故,因此 border 的值为 0不知道下次又会做什么样的梦~
想了解更多编程学习,敬请关注php培训栏目!
以上是這年頭,說自己會Java得會多型的詳細內容。更多資訊請關注PHP中文網其他相關文章!