JavaRush /Java Blog /Random-TW /多態性及其朋友
Viacheslav
等級 3

多態性及其朋友

在 Random-TW 群組發布
多態性是物件導向程式設計的基本原則之一。它允許您利用 Java 強類型的強大功能並編寫可用且可維護的程式碼。關於他的說法已經很多了,但我希望每個人都能從這篇評論中得到一些新的東西。
多態性及其朋友 - 1

介紹

我想我們都知道Java程式語言屬於Oracle。因此,我們的路徑從以下網站開始:www.oracle.com。主頁上有一個「選單」。其中,「文檔」部分有一個「Java」小節。所有與語言基本功能相關的內容都屬於“Java SE 文件”,因此我們選擇這一部分。文件部分將開啟最新版本,但現在“正在尋找不同的版本?” 讓我們選擇選項:JDK8。在頁面上我們會看到很多不同的選項。但我們對學習語言感興趣:「Java 教程學習路徑」。在此頁面上,我們會找到另一個部分:「學習 Java 語言」。這是最神聖的內容,來自 Oracle 的 Java 基礎知識教學。Java 是一種物件導向程式語言 (OOP),因此即使在 Oracle 網站上學習該語言也是從討論「物件導向程式設計概念」的基本概念開始的。從名稱本身就可以清楚看出,Java 專注於處理物件。從「什麼是物件?」小節可以清楚看出,Java 中的物件由狀態和行為組成。想像一下我們有一個銀行帳戶。帳戶中的金額是一種狀態,而處理這種狀態的方法是行為。物件需要以某種方式進行描述(告訴它們可能具有什麼狀態和行為),而這種描述就是類別。當我們建立某個類別的物件時,我們指定該類,這稱為「物件類型」。因此,可以說 Java 是一種強類型語言,正如 Java 語言規範中「第 4 章類型、值和變數」部分所述。Java 語言遵循 OOP 概念並支援使用extends 關鍵字的繼承。為什麼要擴張?因為透過繼承,子類別繼承了父類別的行為和狀態,並且可以補充它們,即 擴充基底類別的功能。也可以使用implements 關鍵字在類別描述中指定介面。當一個類別實作一個介面時,這表示該類別符合某種契約-程式設計師向環境的其餘部分聲明該類別具有某種行為。例如,播放器有各種按鈕。這些按鈕是控製播放器行為的接口,行為會改變播放器的內部狀態(例如音量)。在這種情況下,狀態和行為作為描述將給出一個類別。如果一個類別實作了一個接口,那麼該類別創建的物件不僅可以透過類別來描述,還可以透過接口來描述。讓我們來看一個例子:
public class MusicPlayer {

    public static interface Device {
        public void turnOn();
        public void turnOff();
    }

    public static class Mp3Player implements Device {
        public void turnOn() {
            System.out.println("On. Ready for mp3.");
        }
        public void turnOff() {
            System.out.println("Off");
        }
    }

    public static class Mp4Player extends Mp3Player {
        @Override
        public void turnOn() {
            System.out.println("On. Ready for mp3/mp4.");
        }
    }

    public static void main(String []args) throws Exception{
        // Какое-то устройство (Тип = Device)
        Device mp3Player = new Mp3Player();
        mp3Player.turnOn();
        // У нас есть mp4 проигрыватель, но нам от него нужно только mp3
        // Пользуемся им How mp3 проигрывателем (Тип = Mp3Player)
        Mp3Player mp4Player = new Mp4Player();
        mp4Player.turnOn();
    }
}
類型是一個非常重要的描述。它告訴我們將如何處理該對象,即 我們期望對像有什麼行為。行為就是方法。因此,讓我們了解一下這些方法。在 Oracle 網站上,方法在 Oracle 教學中有自己的部分:「定義方法」。從本文要了解的第一件事:方法簽名是方法的名稱和參數的類型
多態性及其朋友 - 2
例如,當宣告一個方法 public void method(Object o) 時,簽章將是方法的名稱和參數 Object 的型別。傳回類型不包含在簽名中。這很重要!接下來,讓我們編譯我們的原始碼。眾所周知,為此,程式碼必須保存在一個檔案中,該檔案的名稱為類別名,副檔名為 java.lang. Java 程式碼使用「javac」編譯器編譯成某種可以由 Java 虛擬機器 (JVM) 執行的中間格式。這種中間格式稱為字節碼,包含在副檔名為 .class 的檔案中。我們來執行命令進行編譯:javac MusicPlayer.java java程式碼編譯完成後,我們就可以執行它了。使用「java」實用程式啟動,將啟動java虛擬機器進程來執行類別檔案中傳遞的字節碼。讓我們運行命令來啟動應用程式:java MusicPlayer。我們將在螢幕上看到 println 方法的輸入參數中指定的文字。有趣的是,將字節碼放在擴展名為 .class 的檔案中,我們可以使用「javap」實用程式查看它。讓我們執行指令 <ocde>javap -c MusicPlayer:
多態性及其朋友 - 3
從字節碼中我們可以看到,透過指定類別的類型的物件呼叫方法是使用 進行的invokevirtual,並且編譯器已經計算出應該使用哪個方法簽章。為什麼invokevirtual?因為有一個虛方法的呼叫(invoke翻譯為呼叫)。什麼是虛擬方法?這是一個在程式執行期間​​可以覆寫其主體的方法。簡單想像一下,您有一個特定金鑰(方法簽章)和方法主體(程式碼)之間的對應清單。並且鍵和方法體之間的這種對應關係可能會在程式執行過程中發生變化。因此該方法是虛擬的。預設情況下,在 Java 中,非靜態、非最終和非私有的方法是虛擬的。正因為如此,Java 支援物件導向的多型程式設計原則。正如您可能已經了解的那樣,這就是我們今天的評論的內容。

多態性

在 Oracle 網站的官方教學中,有一個單獨的部分:「多態性」。讓我們使用Java 線上編譯器來看看多態性在 Java 中是如何運作的。例如,我們有一些代表Java中數字的抽象類別Number 。它允許什麼?他擁有一些所有繼承人都會擁有的基本技術。任何繼承 Number 的人都會說:“我是一個數字,你可以將我作為一個數字來工作。” 例如,對於任何後繼,您可以使用 intValue() 方法來取得其 Integer 值。如果你看Number的java api,你會發現該方法是抽象的,也就是說,Number的每個後繼者都必須自己實作這個方法。但這為我們帶來了什麼?讓我們來看一個例子:
public class HelloWorld {

    public static int summ(Number first, Number second) {
        return first.intValue() + second.intValue();
    }

    public static void main(String []args){
        System.out.println(summ(1, 2));
        System.out.println(summ(1L, 4L));
        System.out.println(summ(1L, 5));
        System.out.println(summ(1.0, 3));
    }
}
從範例中可以看出,由於多態性,我們可以編寫一個接受任何類型參數作為輸入的方法,該方法將是 Number 的後代(我們無法取得 Number,因為它是一個抽象類別)。與播放器範例的情況一樣,在本例中我們說我們想要使用某些東西,例如 Number。我們知道任何一個數字都必須能夠提供它的整數值。這對我們來說就足夠了。我們不想深入了解特定物件的實作細節,而是希望透過 Number 的所有後代通用的方法來處理該物件。我們可用的方法清單將由編譯時的類型決定(正如我們之前在字節碼中看到的那樣)。在這種情況下,我們的類型將是 Number。從範例中可以看到,我們傳遞了不同類型的不同數字,即 summ 方法將接收 Integer、Long 和 Double 作為輸入。但它們的共同點是它們都是抽象 Number 的後代,因此在 intValue 方法中重寫了它們的行為,因為 每個特定類型都知道如何將該類型轉換為 Integer。這種多態性是透過所謂覆蓋來實現的,英文為Overriding。
多態性及其朋友 - 4
重寫或動態多態性。因此,我們首先儲存包含以下內容的 HelloWorld.java 檔案:
public class HelloWorld {
    public static class Parent {
        public void method() {
            System.out.println("Parent");
        }
    }
    public static class Child extends Parent {
        public void method() {
            System.out.println("Child");
        }
    }

    public static void main(String[] args) {
        Parent parent = new Parent();
        Parent child = new Child();
        parent.method();
        child.method();
    }
}
讓我們做javac HelloWorld.java並且javap -c HelloWorld
多態性及其朋友 - 5
正如您所看到的,在包含方法呼叫的行的字節碼中,指示了對呼叫方法的相同參考invokevirtual (#6)。我們開始做吧java HelloWorld。如我們所看到的,變數parent和child是用Parent類型宣告的,但根據指派給變數的物件(即物件的類型)來呼叫實作本身。在程式執行期間​​(他們也說在運行時),JVM根據物件的不同,在呼叫使用相同簽名的方法時,執行不同的方法。也就是說,使用對應簽署的金鑰,我們先收到一個方法體,然後再收到另一個方法體。取決於變數中的物件。在程式執行時確定將呼叫哪個方法也稱為後期綁定或動態綁定。也就是說,簽章和方法體之間的匹配是動態執行的,這取決於呼叫該方法的物件。當然,您不能覆寫類別的靜態成員(Class member),以及存取類型為 private 或 Final 的類別成員。@Override 註釋也可以為開發人員提供協助。它可以幫助編譯器理解此時我們將重寫祖先方法的行為。如果我們在方法簽章中犯了錯誤,編譯器會立即告訴我們。例如:
public static class Parent {
        public void method() {
            System.out.println("parent");
        }
}
public static class Child extends Parent {
        @Override
        public void method(String text) {
            System.out.println("child");
        }
}
編譯時不會出現錯誤:錯誤:方法不會覆寫或實作超類型中的方法
多態性及其朋友 - 6
重新定義也與「協方差」的概念相關。讓我們來看一個例子:
public class HelloWorld {
    public static class Parent {
        public Number method() {
            return 1;
        }
    }
    public static class Child extends Parent {
        @Override
        public Integer method() {
            return 2;
        }
    }

    public static void main(String[] args) {
        System.out.println(new Child().method());
    }
}
儘管表面上很深奧,但其意義歸結為這樣一個事實:當重寫時,我們不僅可以返回祖先中指定的類型,還可以返回更具體的類型。例如,祖先返回 Number,我們可以返回 Integer - Number 的後代。這同樣適用於方法的 throws 中聲明的異常。繼承人可以重寫該方法並改進拋出的異常。但它們無法擴展。也就是說,如果父進程拋出 IOException,那麼我們可以拋出更精確的 EOFException,但不能拋出 Exception。同樣,您不能縮小範圍,也不能施加額外的限制。例如,您不能新增靜態。
多態性及其朋友 - 7

隱藏

還有一種說法叫做「隱瞞」。例子:
public class HelloWorld {
    public static class Parent {
        public static void method() {
            System.out.println("Parent");
        }
    }
    public static class Child extends Parent {
        public static void method() {
            System.out.println("Child");
        }
    }

    public static void main(String[] args) {
        Parent parent = new Parent();
        Parent child = new Child();
        parent.method();
        child.method();
    }
}
如果你仔細想想,這是一個非常明顯的事情。類別的靜態成員屬於該類,即 到變數的類型。因此,如果 child 的類型為 Parent,那麼該方法將在 Parent 上調用,而不是在 child 上,這是合乎邏輯的。如果我們像之前那樣查看字節碼,我們將看到靜態方法是使用 invokestatic 呼叫的。這向 JVM 解釋說,它需要查看類型,而不是像 invokevirtual 或 invokeinterface 那樣查看方法表。
多態性及其朋友 - 8

重載方法

我們在 Java Oracle 教程中還看到了什麼?在前面學習的「定義方法」一節中,有一些關於重載的內容。這是什麼?在俄語中,這是“方法重載”,這樣的方法稱為“重載”。所以,方法重載。乍一看,一切都很簡單。讓我們開啟一個線上Java編譯器,例如tutorialspoint online java compiler
public class HelloWorld {

	public static void main(String []args){
		HelloWorld hw = new HelloWorld();
		hw.say(1);
		hw.say("1");
	}

	public static void say(Integer number) {
		System.out.println("Integer " + number);
	}
	public static void say(String number) {
		System.out.println("String " + number);
	}
}
所以,這裡一切看起來都很簡單。如 Oracle 教程所述,重載方法(在本例中為 say 方法)在傳遞給方法的參數數量和類型方面有所不同。您不能聲明相同名稱和相同數量的相同類型的參數,因為 編譯器將無法區分它們。值得注意的是一件非常重要的事:
多態性及其朋友 - 9
也就是說,當重載時,編譯器會檢查正確性。這很重要。但是編譯器實際上如何決定需要呼叫某個方法呢?它使用Java語言規範中所描述的「最具體方法」規則:「15.12.2.5.選擇最具體方法」。為了示範它是如何運作的,我們以 Oracle 認證專業 Java 程式設計師為例:
public class Overload{
  public void method(Object o) {
    System.out.println("Object");
  }
  public void method(java.io.FileNotFoundException f) {
    System.out.println("FileNotFoundException");
  }
  public void method(java.io.IOException i) {
    System.out.println("IOException");
  }
  public static void main(String args[]) {
    Overload test = new Overload();
    test.method(null);
  }
}
舉個例子:https://github.com/stokito/OCPJP/blob/master/src/ru/habrahabr/blogs/java/OCPJP1/question1/Overload.j... 如你所見,我們正在透過這個方法為空。編譯器嘗試確定最具體的類型。對像不適合,因為 一切都是從他那裡繼承的。前進。有兩類例外。我們來看看java.io.IOException,發現「Direct Known Subclasses」裡有一個FileNotFoundException。也就是說,事實證明 FileNotFoundException 是最具體的型別。因此,結果將是字串“FileNotFoundException”的輸出。但是,如果我們將 IOException 替換為 EOFException,則結果表明我們在類型樹中的層次結構的相同層級上有兩個方法,也就是說,對於這兩個方法,IOException 都是父方法。編譯器將無法選擇呼叫哪個方法,並會拋出編譯錯誤:reference to method is ambiguous。再舉一個例子:
public class Overload{
    public static void method(int... array) {
        System.out.println("1");
    }

    public static void main(String args[]) {
        method(1, 2);
    }
}
它將輸出 1。這裡沒有問題。int... 類型是一個 vararg https://docs.oracle.com/javase/8/docs/technotes/guides/language/varargs.html ,實際上只不過是“語法糖”,實際上是一個 int 。 .. 數組可以讀取為int[] 數組。如果我們現在加入一個方法:
public static void method(long a, long b) {
	System.out.println("2");
}
那麼它不會顯示 1,而是 2,因為 我們傳遞 2 個數字,並且 2 個參數比 1 個數組更匹配。如果我們加入一個方法:
public static void method(Integer a, Integer b) {
	System.out.println("3");
}
那麼我們仍然會看到 2。因為在這種情況下,基元比 Integer 中的裝箱更精確匹配。但是,如果我們執行,method(new Integer(1), new Integer(2));它將列印 3. Java 中的建構函數與方法類似,並且由於它們也可以用於獲取簽名,因此與重載方法一樣適用相同的「重載解析」規則。Java 語言規範在「8.8.8. 建構子重載」中告訴了我們這一點。方法重載 = 早期綁定(又稱靜態綁定) 您經常會聽到早期綁定和晚期綁定,也稱為靜態綁定或動態綁定。它們之間的差異非常簡單。早期是編譯,晚期是程式執行的時刻。因此,早期綁定(靜態綁定)就是在編譯時決定要對誰呼叫哪個方法。那麼,後期綁定(動態綁定)就是在程式執行時決定要直接呼叫哪個方法。正如我們之前看到的(當我們將 IOException 更改為 EOFException 時),如果我們重載方法,使編譯器無法理解在哪裡進行哪個調用,那麼我們將得到一個編譯時錯誤:對方法的引用不明確。英文中的ambiguously一詞翻譯過來的意思是不明確的或不確定的、不精確的。事實證明,重載是早期綁定,因為 檢查是在編譯時執行的。為了證實我們的結論,讓我們開啟Java語言規範中的「 8.4.9.重載」章節:
多態性及其朋友 - 10
事實證明,在編譯期間,有關參數類型和數量的資訊(在編譯時可用)將用於確定方法的簽章。如果該方法是物件的方法之一(即實例方法),則將在執行時間使用動態方法來尋找(即動態綁定)來確定實際的方法呼叫。為了更清楚地說明這一點,讓我們舉一個與前面討論的類似的例子:
public class HelloWorld {
    public void method(int intNumber) {
        System.out.println("intNumber");
    }
    public void method(Integer intNumber) {
        System.out.println("Integer");
    }
    public void method(String intNumber) {
        System.out.println("Number is: " + intNumber);
    }

    public static void main(String args[]) {
        HelloWorld test = new HelloWorld();
        test.method(2);
    }
}
讓我們將此程式碼儲存到檔案 HelloWorld.java 並使用以下命令對其進行編譯。javac HelloWorld.java 現在讓我們透過執行以下命令來查看編譯器在字節碼中寫入的內容:javap -verbose HelloWorld
多態性及其朋友 - 11
如前所述,編譯器已確定將來將呼叫某些虛擬方法。也就是說,方法體將在運行時定義。但在編譯時,編譯器在這三種方法中選擇了最合適的一種,因此它顯示了數字:"invokevirtual #13"
多態性及其朋友 - 12
這是什麼樣的methodref?這是該方法的連結。粗略地說,這是一些線索,在運行時,Java 虛擬機器實際上可以確定要尋找執行哪個方法。更多詳細內容可參考超級文章:《JVM如何在內部處理方法重載與重寫》。

總結

所以,我們發現Java作為一種物件導向的語言,是支援多態的。多態性可以是靜態的(靜態綁定),也可以是動態的(動態綁定)。透過靜態多態性(也稱為早期綁定),編譯器可以確定應呼叫哪個方法以及在何處呼叫。這允許使用諸如過載之類的機制。透過動態多態性,也稱為後期綁定,基於先前計算的方法簽名,將在運行時根據使用哪個物件(即調用哪個物件的方法)來計算方法。可以使用字節碼來了解這些機制的工作原理。重載會查看方法簽名,並在解決重載時選擇最具體(最準確)的選項。重寫查看類型以確定哪些方法可用,並根據物件呼叫方法本身。以及有關該主題的資料: #維亞切斯拉夫
留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION