1. 程式人生 > >Java 面對對象之多態

Java 面對對象之多態

quest 告訴 true extend 有一個 執行過程 成員變量 既然 nds

閱讀目錄

  • 多態(polymorphism)
  • 向上類型轉換(Upcast)和向下類型轉換(Downcast)
  • instanceof 關鍵字

面向對象第三大特征:多態

多態(polymorphism)

多態是面向對象的重要特性, 簡單點說:“一個接口,多種實現”,就是同一種事物表現出的多種形態。
編程其實就是一個將具體世界進行抽象化的過程,多態就是抽象化的一種體現,把一系列具體事物的共同點抽象出來, 再通過這個抽象的事物, 與不同的具體事物進行對話。

一個程序跑起來需要經過編譯→運行。而多態是一種運行期的行為,不是編譯期的行為。

    • 多態性是 OOP 中的一個重要特性,主要是用來實現動態聯編的,換句話說,就是程序的最終狀態只有在執行過程中才被決定而非在編譯期間就決定了
      。這對於大型系統來說能提高系統的靈活性和擴展性。
    • Java 中如何實現多態?使用多態的好處?
      • 引用變量的兩種類型:
        • 編譯時類型(模糊一點,一般是一個父類)
          • 由聲明時的類型決定。
        • 運行時類型(運行時,具體是哪個子類就是哪個子類)
          • 由實際對應的對象類型決定。

多態的存在要有 3 個必要條件

    (多態是指方法的多態,屬性沒有多態)
    • 要有繼承;
    • 要有方法重寫;
    • 父類引用指向子類對象。

【多態 示例 1】

package com.gta.testoop.polymorphism;

public class Animal {
    public void voice(){
        System.out.println("普通動物叫聲!");
    }
}

class Cat extends Animal{
    //方法重寫
    public void voice() {
        System.out.println("喵喵喵~");
    }
}

class Dog extends Animal{ //方法重寫 public void voice() { System.out.println("汪汪汪~"); } } class Pig extends Animal{ //方法重寫 public void voice() { System.out.println("哼哼哼~"); } }

測試類:

(如果沒有采用多態,如下所示)

public class Test {
    //如果沒有多態,采用方法重載
    public static void testAnimalVoice(Cat c){
        c.voice();
    }
    
    public static void testAnimalVoice(Dog d){
        d.voice();
    }
    
    public static void testAnimalVoice(Pig p){
        p.voice();
    }
    
    public static void main(String[] args) {
        // TODO Auto-generated method stub
        Cat c=new Cat();
        testAnimalVoice(c);
    }

}

采用多態,如下所示:

public class Test {
    //采用多態
    public static void testAnimalVoice(Animal a){//Animal是a的編譯時類型
        a.voice();
    }
    
    public static void main(String[] args) {
        //①向上類型轉換
        Cat c=new Cat();//創建一個Cat對象c,Cat是Animal的子類;
//        Animal a=c;
        testAnimalVoice(c);//Cat是運行時類型,實際是執行Animal a=c;再執行testAnimalVoice方法。
    
        //上面①等價於②:向下類型轉換
        Animal c1=new Cat();//父類引用指向子類對象
        testAnimalVoice(c1);
        
    }
}

如果子類有新方法:

技術分享圖片

技術分享圖片

此處報錯,Animal 沒有編譯。Animal 類裏沒有 catchMouse 方法,c1 引用是 Animal 類,所以找不到該方法,編譯通不過。

可以采用強制轉型,將 c1 再轉回 Cat 型。

技術分享圖片

多態:父類型的引用可以指向子類型的對象。

  比如 Parent p = new Child();

    當使用多態方式調用方法時,首先檢查父類中是否有該方法,如果沒有,則編譯錯誤;

    如果有,再去調用子類的該同名方法。

  (註意此處,靜態 static 方法屬於特殊情況,靜態方法只能繼承,不能重寫 Override,如果子類中定義了同名同形式的靜態方法,它對父類方法只起到隱藏的作用。調用的時候用誰的引用,則調用誰的版本

向上類型轉換(Upcast)和向下類型轉換(Downcast)

技術分享圖片

對象轉型分為兩種:一種叫向上轉型 (父類對象的引用或者叫基類對象的引用指向子類對象,這就是向上轉型),另一種叫向下轉型。轉型的意思是:如把 float 類型轉成 int 類型,把 double 類型轉成 float 類型,把 long 類型轉成 int 類型,這些都叫轉型。把一種形式轉成另外一種形式就叫轉型。除了基礎數據類型的轉型之外(基礎數據類型的轉型:大的可以轉成小的,小的也可以轉成大的。),對象領域裏面也有對象之間的轉型。

(1)向上類型轉換(Upcast):將子類型轉換為父類型。

  對於向上的類型轉換,不需要顯示指定,即不需要加上前面的小括號和父類類型名。

技術分享圖片

(2)向下類型轉換(Downcast):將父類型轉換為子類型。

  對於向下的類型轉換,必須要顯式指定,即必須要使用強制類型轉換

技術分享圖片

並且父類型的引用必須指向子類的對象,即指向誰才能轉換成誰。

  不然也會編譯出錯:

技術分享圖片

技術分享圖片

  因為父類引用指向的是 Cat 類的對象,而要強制轉換成 Dog 類,這是不可能的。

1.1 對象轉型實例一

package javastudy.summary;

/**
 * 父類Animal
 */
class Animal {

    public String name;

    public Animal(String name) {
        this.name = name;
    }
}

/**
 * 子類Cat繼承Animal
 */
class Cat extends Animal {

    /**
     * Cat添加自己獨有的屬性
     */
    public String eyeColor;

    public Cat(String n, String c) {
        super(n);//調用父類Animal的構造方法
        this.eyeColor = c;
    }
}

/**
 * 子類Dog繼承Animal
 */
class Dog extends Animal {
    /**
     * Dog類添加自己特有的屬性
     */
    public String furColor;

    public Dog(String n, String c) {
        super(n);//調用父類Animal的構造方法
        this.furColor = c;
    }

}

/**
 * 下面是這三個類的測試程序
 */
public class TestClassCast {

    /**
     * @param args
     */
    public static void main(String[] args) {

        Animal a = new Animal("name");
        Cat c = new Cat("catname","blue");
        Dog d = new Dog("dogname", "black");
        /**
         * a instanceof Animal這句話的意思是a是一只動物嗎?
         * a是Animal這個類裏面的是一個實例對象,所以a當然是一只動物,其結果為true。
         */
        System.out.println(String.format("a instanceof Animal的結果是%s",a instanceof Animal));//true
        /**
         * c是Cat類的實例對象的引用,即c代表的就是這個實例對象,
         * 所以“c是一只動物”打印出來的結果也是true。
         * d也一樣,所以“d是一只動物”打印出來的結果也是true。
         */
        System.out.println(String.format("c instanceof Animal的結果是%s",c instanceof Animal));//true
        System.out.println(String.format("d instanceof Animal的結果是%s",d instanceof Animal));//true
        /**
         * 這裏判斷說“動物是一只貓”,不符合邏輯,所以打印出來的結果是false。
         */
        System.out.println(String.format("a instanceof Cat的結果是%s",a instanceof Cat));
        /**
         * 這句話比較有意思了,a本身是Animal類的實例對象的引用,
         * 但現在這個引用不指向Animal類的實例對象了,而是指向了Dog這個類的一個實例對象了,
         * 這裏也就是父類對象的引用指向了子類的一個實例對象。
         */
        a = new Dog("bigyellow", "yellow");
        System.out.println(a.name);//bigyellow
        /**
         * 這裏的furColor屬性是子類在繼承父類的基礎上新增加的一個屬性,是父類沒有的。
         * 因此這裏使用父類的引用對象a去訪問子類對象裏面新增加的成員變量是不允許的,
         * 因為在編譯器眼裏,你a就是Animal類對象的一個引用對象,你只能去訪問Animal類對象裏面所具有的name屬性,
         * 除了Animal類裏面的屬性可以訪問以外,其它類裏面的成員變量a都沒辦法訪問。
         * 這裏furColor屬性是Dog類裏面的屬性,因此你一個Animal類的引用是無法去訪問Dog類裏面的成員變量的,
         * 盡管你a指向的是子類Dog的一個實例對象,但因為子類Dog從父類Animal繼承下來,
         * 所以new出一個子類對象的時候,這個子類對象裏面會包含有一個父類對象,
         * 因此這個a指向的正是這個子類對象裏面的父類對象,因此盡管a是指向Dog類對象的一個引用,
         * 但是在編譯器眼裏你a就是只是一個Animal類的引用對象,你a就是只能訪問Animal類裏面所具有的成員變量,
         * 別的你都訪問不了。
         * 因此一個父類(基類)對象的引用是不可以訪問其子類對象新增加的成員(屬性和方法)的。
         */
        //System.out.println(a.furColor);
        System.out.println(String.format("a指向了Dog,a instanceof Animal的結果是%s",a instanceof Animal));//true
        /**
         * 這裏判斷說“a是一只Dog”是true。
         * 因為instanceof探索的是實際當中你整個對象到底是什麽東西,
         * 並不是根據你的引用把對象看出什麽樣來判斷的。
         */
        System.out.println(String.format("a instanceof Dog的結果是%s",a instanceof Dog));//true
        /**
         * 這裏使用強制轉換,把指向Animal類的引用對象a轉型成指向Dog類對象的引用,
         * 這樣轉型後的引用對象d1就可以直接訪問Dog類對象裏面的新增的成員了。
         */
        Dog d1 = (Dog)a;
        System.out.println(d1.furColor);//yellow
    }

}

運行結果:

技術分享圖片

對象轉型內存分析

技術分享圖片

  在內存中可以看到,指向 Dog 類實例對象的引用對象 a 是一個 Animal 類型的引用類型,這就比較有意思了,Animal 類型指向了 Dog 這個對象,那麽,在程序的眼睛裏會把這只 Dog 當成一只普通的 Animal,既然是把 Dog 當成一只普通的 Animal,那麽 Dog 類裏面聲明的成員變量 furColor 就不能訪問了,因為 Animal 類裏面沒有這個成員變量。因此,從嚴格意義上來講,這個 a 眼裏只看到了這個子類對象裏面的父類對象 Animal, 因此能訪問得到的也只是這個 Animal 對象裏面的 name 屬性,而這個 Animal 對象外面的 furColor 屬性是訪問不到的,雖然 Dog 對象確實有這個屬性存在,但 a 就是看不到,a 門縫裏看 Dog——把 Dog 看扁了,不知道 Dog 還有 furColor 這個屬性存在,因此 a 訪問不了 furColor 屬性,因此從嚴格意義上來講,a 指向的只是這個 Dog 對象裏面的 Animal 對象,也就是黃色箭頭指向的那部分,a 就只看到了 Dog 裏面這部分,而 Dog 外面的部分都看不到了。這就是父類引用指向子類對象,父類引用指向子類對象的時候,它看到的只是作為父類的那部分所擁有的屬性和方法,至於作為子類的那部分它沒有看到。

  如果真的想訪問 Dog 對象的 furColor 屬性,那就采用對象轉型的辦法,把父類對象的引用轉型成子類對象的引用。Dog d1 = (Dog)a; 這裏采用的就是對象轉型的辦法,把 a 強制轉換成一只 Dog 對象的引用,然後將這個引用賦值給 Dog 型的引用變量 d1,這樣 d1 和 a 都是指向堆內存裏面的 Dog 對象了,而且 d1 指向的就是這只 Dog 所有的部分了,通過這個 d1 就可以訪問 Dog 對象裏面所有的成員了。

1.2 對象轉型實例二

public class TestClassCast {

    public void  f(Animal a) {
        System.out.println(a.name);
        if (a instanceof Cat) {
            Cat cat = (Cat)a;
            System.out.println(cat.eyeColor+" eye");
        }else if (a instanceof Dog) {
            Dog dog = (Dog)a;
            System.out.println(dog.furColor+" fur");
        }
    }
    
    /**
     * @param args
     */
    public static void main(String[] args) {
        Animal a = new Animal("name");
        Cat c = new Cat("catname","blue");
        Dog d = new Dog("dogname", "black");
        TestClassCast testClassCast = new TestClassCast();
        testClassCast.f(a);
        testClassCast.f(c);
        testClassCast.f(d);
    }
}

這裏的這些代碼是對前面聲明的三個類 Animal,Dog,Cat 測試的延續,這裏我們在 TestClassCast 這裏類裏面測試這個三個類,這裏我們在 TestClassCast 類裏面 new 了一個 testClassCast 對象,為的是調用 TestClassCast 類裏面聲明的 f(Animal a) 這個方法,這個 f() 方法裏面的參數類型是 Animal 類型,如果是 Animal 類型的參數,那麽我們可以把這個 Animal 類型的子類對象作為參數傳進去,這是可以的。如把一只 Dog 或者是一只 Cat 丟進 f() 方法裏面這都是可以的,因為 Dog 和 Cat 也是 Animal。因此當程序執行到 testClassCast.f(a);,testClassCast.f(c);,testClassCast.f(d); 的時候,因為 f() 方法裏面的參數是 Animal 類型的,所以我們可以把一個 Animal 對象傳進去,除此之外,我們還可以直接把從 Animal 類繼承下來的 Dog 類和 Cat 類裏面的 Dog 對象和 Cat 對象作為實參傳遞過去,即是把 Animal 類型的子類對象作為參數傳進去。這裏就體現出了繼承和父類對象的引用可以指向子類對象的好處了,如果說沒有繼承關系的存在,如果說父類的引用不可以指向子類對象,那麽我們就得要在 Test 類裏面定義三個 f() 方法了,即要定義這樣的 f() 方法:
f(Animal a)、f(Dog d)、f(Cat c) 分別用來處理 Animal、Dog 和 Cat,使用三個方法來處理,將來程序的擴展起來就不是很容易了,因為面向對象可以幫助我們這些年來編程苦苦追求的一個境界是可擴展性比較好。可擴展性比較好的一個典型例子就是說當你建好一個建築之後或者是你寫好這個程序之後,把這個主建築給建好了,將來你要加一些其他的功能的時候,盡量不要去修改主結構,這叫可擴展性好,你蓋了一座大樓,你現在要在大樓的旁邊添加一個廚房,那你在它旁邊一蓋就行了,如果有人告訴你,我添加一個廚房我需要把你整個大樓的主要柱子都給拆了然後再蓋一遍,這你幹嗎,肯定不幹。如果結構設計成這樣,那就是設計得不好,可擴展性不好。所以這裏如果要把 f() 方法寫成三個重載的 f() 方法,那麽將來我輸出一只鳥的時候又得要添加一個 f(Bird b) 方法來處理鳥。這樣擴展起來就太麻煩了,因為每處理一只動物都要添加一個新的方法,但是如果存在繼承關系,如果父類對象的引用可以指向子類對象,那擴展起來就簡單了,你可以把處理動物的方法寫在一個方法 f(Animal a) 裏面就夠了,因為所有動物的種類都是從 Animal 類繼承下來,因此給 f() 方法傳遞 Animal 類型的參數的時候可以直接把這個 Animal 類的子類對象傳進去,這樣不管是要增加什麽動物的輸出,我都可以調用 f(Animal a) 方法去處理,這種擴展性比每次都要增加一個新的處理方法的擴展性要好得多,這就是繼承的一個好處,這就是對象轉型對於可擴展性來的好處:“對象的引用可以指向子類對象”,是因為面向對象的編程裏面存在這樣的繼承關系,使得程序的可擴展性比較好。

  對象轉型可以使父類對象的引用可以指向子類對象,給程序帶來了比較好的可擴展性:我們可以在一個方法的參數裏面定義父類的引用,然後實際當中傳的時候傳的是子類的對象,然後我們再在方法裏面判斷這個傳過來的子類對象到底屬於哪個子類,然後再去執行這個子類裏面的方法或者調用這個子類裏面的成員變量,因此程序的可擴展性比單獨定義好多個方法要好一些。不過這個可擴展性還沒有達到最好,使用多態就可以讓程序的擴展性達到極致。

instanceof 關鍵字

instanceof 是 Java 的一個二元操作符,和 ==,>,< 是同一類東東。由於它是由字母組成的,所以也是 Java 的保留關鍵字。它的作用是測試它左邊的對象是否是它右邊的類的實例,返回 boolean 類型的數據

技術分享圖片

相關鏈接:

java 基礎學習總結——多態 (動態綁定)

面向對象編程 (十四)——面向對象三大特性之多態①

JAVA的多態用幾句話能直觀的解釋一下嗎?

JAVA 多態 | 菜鳥教程

Java 面對對象之多態