1. 程式人生 > 實用技巧 >不是吧?不會多型,你還說自己會Java

不是吧?不會多型,你還說自己會Java

大家好,我是小菜,一個渴望在網際網路行業做到蔡不菜的小菜。可柔可剛,點贊則柔,白嫖則剛! 死鬼~看完記得給我來個三連哦!

本文主要介紹 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 先輸出的疑惑。那麼我們就帶著疑惑,整起!

多型

在面向物件的程式設計語言中,多型是繼資料抽象和繼承之後的第三種基本特徵。

多型不但能夠改善程式碼的組織結構和可讀性,還能夠建立可擴充套件的程式。多型的作用就是消除型別之間的耦合關係

1. 向上轉型

根據里氏代換原則:任何基類可以出現的地方,子類一定可以出現。

物件既可以作為它自己本身的型別使用,也可以作為它的基型別使用。而這種吧對某個物件的引用視為對其基型別的引用的做法被稱作為 - 向上轉型。因為父類在子類的上方,子類要引用父類,因此稱為 向上轉型

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),過載用得挺溜嘛小夥子,但是未免太麻煩了。懶惰才是開發人員的天性。

因此這樣就有了多型的產生

2.顯露優勢

方法呼叫中分為 靜態繫結動態繫結。何為繫結:將一個方法呼叫同一個方法主體關聯起來被稱作繫結。

  • 靜態繫結:又稱為前期繫結。是在程式執行前進行把繫結。我們平時聽到"靜態"的時候,不難免想到static關鍵字,被static關鍵字修飾後的變數成為靜態變數,這種變數就是在程式執行前初始化的。前期繫結是面向過程語言中預設的繫結方式,例如 C 語言只有一種方法呼叫,那就是前期繫結。

引出思考:

public static void start(Animal animal) {
    animal.eat();
}
複製程式碼

start()方法中傳入的是Animal 的物件引用,如果有多個Animal的匯出類,那麼執行eat()方法的時候如何知道呼叫哪個方法。如果通過前期繫結那麼是無法實現的。因此就有了後期繫結

  • 動態繫結:又稱為後期繫結。是在程式執行時根據物件型別進行繫結的,因此又可以稱為執行時繫結。而 Java 就是根據它自己的後期繫結機制,以便在執行時能夠判斷物件的型別,從而呼叫正確的方法。

小結:

Java 中除了 staticfinal 修飾的方法之外,都是屬於後期繫結

合理即正確

顯然通過動態繫結來實現多型是合理的。這樣子我們在開發介面的時候只需要傳入 基類 的引用,從而這些程式碼對所有 基類 的 匯出類 都可以正確的執行。

其中MonkeyPigDog皆是Animal的匯出類

Animal animal = new Monkey() 看上去不正確的賦值,但是上通過繼承,Monkey就是一種Animal,如果我們呼叫animal.eat()方法,不瞭解多型的小夥伴常常會誤以為呼叫的是Animaleat()方法,但是最終卻是呼叫了Monkey自己的eat()方法。

Animal作為基類,它的作用就是為匯出類建立公用介面。所有從Animal繼承出去的匯出類都可以有自己獨特的實現行為。

可擴充套件性

有了多型機制,我們可以根據自己的需求對系統新增任意多的新型別,而不需要過載void start(Animal animal)方法。

在一個設計良好的OOP程式中,大多數或者所有方法都會遵循start()方法的模型,只與基類介面同行,這樣的程式就是具有可擴充套件性的,我們可以通過從通用的基類繼承出新的資料型別,從而新增一些功能,那些操縱基類介面的方法就不需要任何改動就可以應用於新類。

失靈了?

我們先來複習一下許可權修飾符:

作用域當前類用一個package子孫類其他package
public
protected ×
default × ×
private × × ×
  • 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.fieldSon.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()
*/
複製程式碼

靜態方法是與類相關聯,而非與物件相關聯

3.構造器與多型

首先我們需要明白的是構造器不具有多型性,因為構造器實際上是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()方法,這個時候就採用了動態繫結機制,呼叫了squarecal(),但這個時候border這個變數尚未進行初始化,int 型別的預設值為 0,因此就有了square.cal(), border = 0的輸出。看到這裡,小夥伴們是不是有種撥開雲霧見青天的感覺!

這組程式碼初始化的實際過程為:

  • 在其他任何事物發生之前,將分配給物件的儲存空間初始化成二進位制的零
  • 呼叫基類構造器時,會呼叫被覆蓋後的cal()方法,由於步驟1的緣故,因此 border 的值為 0
  • 按照宣告的順序呼叫成員的初始化方法
  • 呼叫匯出類的構造器主體

呼~終於複習完多型了,幸好是夢,沒人發現我的菜。不知道電腦手機前的你,是否跟小菜一樣呢,如果是的話趕緊跟小菜一起復習,不讓別人發現自己還不會多型哦!

不知道下次又會做什麼樣的夢~


作者:蔡不菜丶
連結:https://juejin.im/post/6871890430284267534
來源:掘金
著作權歸作者所有。商業轉載請聯絡作者獲得授權,非商業轉載請註明出處。