Thinking In Java筆記(第八章 多態)

第八章 多態

    在面向對象的程序設計語言中,多態是繼抽象和技能之後的第三種基本特徵。多態不但能夠改善代碼的組織結構和可讀性,還能夠創建可擴展的程序。

     多態的作用是用來消除類型之間的耦合關係。

8.1 再論向上轉型

    將某個對象的引用視爲對其基類對象的做法被稱作向上轉型。但是這樣做也有問題。看如下的例子:
public enum Note {
MIDDLE_C, C_SHARP, B_FlAT;
}

class Instrument {
    public void play(Note n) {
        System.out.println("Instrument.play()");
    }
}

class Wind extends Insrument {
    public void play(Note n) {
        System.out.println("wind.play() + " n);
    }
}

public class Music {
    public static void tune(Instrument i) {
        i.play(Note.MIDDLE_C);
    }
    public static void main(String[] args) {
        Wind flute = new Wind();
        tune(flute);    
    }
}

output: wind.play() MIDDLE_C

    Music.tune()方法接受一個Instrument引用,同時也接受任何導出自Instrument類。當tune()方法中接受到一個wind引用時,會自動向上轉型,這樣做時允許的,因爲instrument中的接口再wind中都存在。從wind向上轉型會縮小接口,但是不會小於Instrument的接口。

8.1.1 忘記對象類型

    如果讓tune()方法直接接受一個wind引用作爲自己的參數,表面上更加直觀,但是每加入一種新的樂器(也就是Instrument的子類),相應的tune方法就需要重載一次。但是如果利用了多態,將instrument的多個導出類引用看作時instrument的引用,就會輕鬆很多。

8.2 轉機

    在運行上面的程序之後,傳入instrument的導出類引用給tune方法,它也能識別到底是哪個導出類,很多人都會奇怪,編譯器怎麼知道的呢?其實編譯器根本就不知道。。。。

8.2.1 方法調用綁定

    將一個方法調用同一個方法主體關聯起來被稱作綁定。綁定分爲前期綁定和後期綁定。

  • 前期綁定:面向過程中不需要選擇就默認綁定的方式。
  • 後期綁定:運行時根據對象的類型進行綁定。也稱作動態綁定或者運行時綁定。

    Java中除了static方法和final方法(private方法屬於final方法)之外,其他所有的方法都是後期綁定的。

8.2.2 產生正確的行爲

    Java中所有方法都是通過動態綁定實現多態,我們可以只編寫與基類打交道的程序代碼了,並且這些代碼都可以對所有導出類正確的運行。例如:

class Shape {   
    public void draw() {}
    public void erase() {}
}

class Circle extends Shape { 
    public void draw() {
        System.out.println("Circle.draw()");
    }

    public void erase() {   
        System.out.println("Circle.erase()");
    }
}

class Square extends Shape { 
    public void draw() { 
        System.out.println("Square.draw()");
    }
    public void erase() { 
        System.out.println("Square.erase()");
    }
}

class Triangle extends Shape { 
    public void draw() { 
        System.out.println("Triangle.draw()");
    }
    public void erase() { 
        System.out.println("Triangle.erase()");
    }
}

class RandomShapeGenerator { 
    private Random rand = new Random(47);
    public Shape next() {
        switch(rand.nextInt(3)) { 
            default:
            case 0: return new Circle();
            case 1: return new Square();
            case 2: return new Triangle();
        }
    }
}

public class Test { 
    public static void main(String[] args) { 
        RandomShapeGenerator rsg = new RandomShapeGenerator();
        Shape[] s = new Shape[9];
        for(int i = 0; i < s.length; i++) { 
            s[i] = rsg.next();
        }
        for(Shape shp : s)
            shp.draw();
    }
}

輸出:
Triangle.draw()
Triangle.draw()
Square.draw()
Triangle.draw()
Square.draw()
Triangle.draw()
Square.draw()
Triangle.draw()
Circle.draw()

    上面的例子中,向上轉型發生在return中,每次返回Circle、Square、Triangle三個其中的一個引用,但是我們通過next()方法獲得的總是一個通用的Shape引用,再來利用動態綁定實現多態。

8.2.3 可擴展性

    針對上面的兩種方法(draw和erase),任何Shape的導出類,覆蓋了這兩個接口,利用動態綁定,我們都能夠很好的對其進行使用,不用管每個導出類是如何實現的。

8.2.4 缺陷:對私有方法的覆蓋

    例如:

public class PrivateOverride {
    private void f() {
        System.out.println("private f()");
    }
    public static void main(String[] args) {
        Private Override po = new Deived();
        po.f(); 
    }
}

class Derived extends PrivateOverride {
    public void f() {
        System.out.println("public f()");
    }   
}

    像上面那樣做,我們期待的結果是”public f()”, 但是結果爲”private f()”,因爲private屬於final類方法,其不可以被覆蓋,而且屬於前期綁定,而非動態綁定,因此無法實現動態綁定。

8.2.5 缺陷:域與靜態方法

    一旦瞭解了多態機制,可能會認爲所有事物都可以多態的發生,然而只有普通方法才表現出多態。如果直接訪問某個域,將不表現爲多態。例如:

class Super {
    public int field = 0;
    public int getField() {
        return field;
    }
}
class Sub extends Super {
    public int field = 1;
    public int getField() {
        return field;
    }
    public int getSuperField() {
        return super.field;
    }
}
public class JavaTest{
    public static void main(String[] args) {

        Super sup = new Sub(); //向上轉型
        //這裏在直接訪問域的時候,並沒有出現理想中的多態
        System.out.println("sup.field: " + sup.field + "\nsup.getField: " + sup.getField() + "\n");

        Sub sub = new Sub();
        System.out.println("sub.field: " + sub.field + "\nsub.getField: " + sub.getField() + "\nsub.getSuperField():" + sub.getSuperField());

    }
}
輸出爲:
sup.field: 0
sup.getField: 1

sub.field: 1
sub.getField: 1
sub.getSuperField():0

    當Sub對象轉型爲Super引用時,任何域訪問操作都將由編譯器解析,由於多態採用的是動態綁定,而不是靠編譯器,所以無法完成多態。

    在上面的例子中,一個sub對象中有兩個叫做field的域(Super.field和Sub.field),然而在引用sub中的field時所產生的默認域並非Super版本的field,因此必須顯示的指明super.field。

8.3 構造器和多態

    通常構造器不同於其他的方法,涉及到多態的時候也是如此。儘管構造器並不具有多態性(實際上構造器是static方法,只不過是隱式的static聲明)。

8.3.1 構造器的調用順序

    在之前的[學習筆記](“http://blog.csdn.net/jing_unique_da/article/details/45530563“)中也提到過了初始化的順序,調用的順序是:

  1. 調用基類的構造器。這個步驟一直遞歸下去,直至遞歸到根類,再開始從根嚮導出類開始初始化。
  2. 按聲明順序調用成員的初始化方法。
  3. 調用導出類構造器的主體。

8.3.3 構造器內部的多態方法的行爲

    現在來分析在一個構造器內部調用正在構造的對象的某個動態綁定方法時,會發生什麼事情。例如:

class Super {
    public int field;
    public Super() {
        System.out.println("Super() Before");
    getField();
        System.out.println("Super() after");
    }

    public int getField() {
        System.out.println("Super() " + field);
    return field;
    }
}
class Sub extends Super {
    public int field = 1;
    public Sub(int i) {
        field = i;
        System.out.println("Sub() " + field);
    }
    public int getField() {
        System.out.println("Sub() " + field);
        return field;
    }
}
public class JavaTest{
    public static void main(String[] args) {
        new Sub(5);
    }
}
輸出爲:
Super() Before
Sub() 0
Super() after
Sub() 5

    這裏的getField()方法被覆蓋了,但是在Super類的構造器中調用的getField方法並不是Super的,而是覆蓋之後的方法,而且結果也不是5,而是0。原因在於前面講述的構造順序不完整。初始化的實際過程是:

  1. 在其他任何事情發生之前,先將分配給對象的存儲空間初始化成二進制的零。
  2. 遞歸調用基類的構造器。
  3. 按照聲明的順序調用成員的初始化方法。
  4. 調用導出類的構造器主體。

8.4 協變返回類型

    Java SE5中添加了協變返回類型,也就是在導出類中的被覆蓋方法可以返回基類方法的返回類型的某種導出類型,話有些繞口,直接上例子:

class Grain {
    public String toString() {
        return "Grain";
    }
}
class Wheat extends Grain {
    public String toString() {
        return "Wheat";
    }
}
class Mill {
    Grain process() {
        return new Grain();
    }
}

class WheatMill extends Mill {
    Wheat process() {
        return new Wheat();
    }
}
public class JavaTest{
    public static void main(String[] args) {
        Mill m = new Mill();
        Grain g = m.process();
        System.out.println(g);

        m = new WheatMill();
        g = m.process();
        System.out.println(g);
    }
}
結果:
Grain
Wheat
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章