Java 之路 (八) -- 多態(tài)(向上轉(zhuǎn)型、多態(tài)稚新、綁定勘伺、構(gòu)造器與多態(tài)、協(xié)變返回類(lèi)型褂删、向下轉(zhuǎn)型)

多態(tài) 是繼 數(shù)據(jù)抽象繼承 之后的第三種基本特征飞醉。
多態(tài)也稱(chēng)作動(dòng)態(tài)綁定、后期綁定或運(yùn)行時(shí)綁定屯阀。

多態(tài)的一些具象表現(xiàn)缅帘。

  • 允許不同類(lèi)的對(duì)象對(duì)同一消息做出響應(yīng)
  • 同一個(gè)行為具有多個(gè)不同表現(xiàn)形式或形態(tài)的能力
  • 只有在運(yùn)行時(shí)才會(huì)知道引用變量所指向的具體實(shí)例對(duì)象

封裝:通過(guò)合并特征和行為創(chuàng)建新的數(shù)據(jù)類(lèi)型。
實(shí)現(xiàn)隱藏:通過(guò)將細(xì)節(jié)私有化难衰,把接口和實(shí)現(xiàn)分離開(kāi)來(lái)
多態(tài):消除類(lèi)型之間的耦合關(guān)系钦无。


1. 再論向上轉(zhuǎn)型

上一章中我們提到了,對(duì)象及可以作為自己本身的類(lèi)型使用盖袭,也可以作為ita的基類(lèi)型來(lái)使用失暂。而這種把對(duì)某個(gè)對(duì)象的引用視為對(duì)其基類(lèi)型的引用的做法被稱(chēng)作 向上轉(zhuǎn)型彼宠。

我們看下面這個(gè)例子:

class Instrument {
    public void play(String song){
        System.out.println("Instrument.play() " + song);
    }
}
class Piano extends Instrument{
    //重寫(xiě)方法
    public void play(String song){
        System.out.println("Piano.play() " + song);
    }
}
public class Music {
    public static void tune(Instrument i){
        //...
        i.play("Fade");
    }
    public static void main(String[] args){
        Piano piano = new Piano;
        tune(piano);
    }
}
//輸出結(jié)果為 Piano.play() Fade

我們通過(guò)向上轉(zhuǎn)型,將子類(lèi) Paino 類(lèi)型的引用傳入了接收 父類(lèi) Instrument 類(lèi)型引用參數(shù)的 tune() 方法中弟塞,這是沒(méi)有什么問(wèn)題的凭峡,不過(guò)有一個(gè)疑問(wèn):為什么不讓 tune() 方法直接接收一個(gè) Piano 參數(shù)呢?
乍一看下决记,似乎讓 tune() 方法接收一個(gè) Piano 引用作為參數(shù)更符合常理摧冀,但是這樣會(huì)導(dǎo)致一個(gè)嚴(yán)重的問(wèn)題:

  • 如果這樣做的話,那么每個(gè) Instrument 的子類(lèi)型都要寫(xiě)一個(gè)新的 tune() 方法
    • 這會(huì)造成大量多余的編程
    • 同時(shí)霉涨,假如某個(gè)子類(lèi)忘記修改某個(gè)方法按价,編譯器不會(huì)報(bào)任何錯(cuò)誤,此時(shí)會(huì)出現(xiàn)一些隱患

結(jié)論是:這種情況下如果我們只寫(xiě)一個(gè)簡(jiǎn)單方法笙瑟,它僅僅接收基類(lèi)作為參數(shù)楼镐,而不是特殊的導(dǎo)出類(lèi),事情就迎刃而解了往枷。當(dāng)然這正式 多態(tài) 所允許的框产。

但是不能只知其然不知其所以然,下面就仔細(xì)分析一下上述結(jié)論的依據(jù)是什么错洁。


2. 綁定

我們?cè)賮?lái)分析一下 tune() 方法:

public static void tune(Instrument i){
        //...
        i.play("Fade");
}

前面我們提到 tune() 傳參的時(shí)候秉宿,將 Piano 向上轉(zhuǎn)型作為 Instrument 使用,但與此同時(shí)問(wèn)題出現(xiàn)了:編譯器是如何知道這個(gè) Instrument 引用指向的是 Piano 對(duì)象屯碴?嗯描睦,實(shí)際上,編譯器無(wú)法得知(WTF导而?)忱叭。

為了深入理解這個(gè)問(wèn)題,我們需要研究一下 綁定今艺。

2.1 什么是綁定

綁定 即將一個(gè)方法調(diào)用同一個(gè)方法主題關(guān)聯(lián)起來(lái)

  • 若在程序執(zhí)行前進(jìn)行綁定韵丑,叫做 前期綁定

    如果有前期綁定的話,是由編譯器和連接程序?qū)崿F(xiàn)

  • 如果在運(yùn)行時(shí)根據(jù)對(duì)象的類(lèi)型進(jìn)行綁定虚缎,叫做 后期綁定/動(dòng)態(tài)綁定/運(yùn)行時(shí)綁定

    后期綁定使得編譯器一直不知道對(duì)象的類(lèi)型撵彻,但是方法調(diào)用機(jī)制通過(guò)安置的某種“類(lèi)型信息”,能找到正確的方法體实牡,并加以調(diào)用陌僵。

在 Java 中,除了 static 方法和 final 方法以外铲掐,其他所有的方法都是后期綁定拾弃。這點(diǎn)很關(guān)鍵。

關(guān)于把一個(gè)方法聲明為 final 的作用摆霉,在前面一章也提過(guò)了:

  1. 防止被覆蓋
  2. 出于性能考慮 -- 有效的關(guān)閉動(dòng)態(tài)綁定
    但是應(yīng)該從設(shè)計(jì)方面考慮是否使用 final 方法,而非出于性能的考慮。

2.2 多態(tài)的正確實(shí)踐

從上面分析携栋,我們得知:"Java 中的方法都是通過(guò)動(dòng)態(tài)綁定來(lái)實(shí)現(xiàn)多態(tài)的"搭盾,這樣一來(lái),我們編寫(xiě)代碼時(shí)就只需要和基類(lèi)打交道了婉支,這些代碼自然適用于所有導(dǎo)出類(lèi)鸯隅。換個(gè)說(shuō)法,發(fā)送消息給某個(gè)對(duì)象向挖,讓該對(duì)象去斷定應(yīng)該做什么事蝌以。

我們來(lái)舉個(gè)“幾何形狀”的例子:有一個(gè)基類(lèi) Shape,以及多個(gè)導(dǎo)出類(lèi)何之,如 Circle跟畅、Square、Triangle:

向上轉(zhuǎn)型:Shape s = new Circle()
這里創(chuàng)建了一個(gè) Circle 對(duì)象溶推,并把得到的引用立即賦值給 Shape徊件,能這么做是因?yàn)橥ㄟ^(guò)繼承,Circle 就是一種 Shape蒜危。

繼承表示 is-a 的關(guān)系

此時(shí)如果調(diào)用基類(lèi)方法 s.draw()虱痕,雖然 s 是一個(gè) Shape 引用,但是編譯器實(shí)際上并非調(diào)用 Shape.draw()辐赞,而是由于后期綁定(多態(tài))部翘,會(huì)正確的調(diào)用 Circle.draw() 方法。

在編譯時(shí)响委,編譯器不需要獲得人為添加的任何特殊信息就能進(jìn)行正確的調(diào)用新思。后期綁定 會(huì)替我們進(jìn)行正確調(diào)用。

多態(tài)方法調(diào)用允許一種類(lèi)型表現(xiàn)出與其他相似類(lèi)型之間的區(qū)別晃酒,這種區(qū)別根據(jù)方法行為的不同而表示出來(lái)表牢。

2.3 良好的可擴(kuò)展性

在一個(gè)設(shè)計(jì)良好的 OOP 程序中,大多數(shù)或者所有方法都會(huì)只與基類(lèi)接口通信贝次。這樣的程序是可擴(kuò)展的崔兴,因?yàn)榭梢詮耐ㄓ玫幕?lèi)繼承出新的數(shù)據(jù)類(lèi)型,從而新添一些功能蛔翅,那些操縱基類(lèi)接口的方法不需要任何改動(dòng)就可以應(yīng)用于新類(lèi)敲茄。

回到上面的 “樂(lè)器”(Instrument) 示例。由于多態(tài)機(jī)制山析,我們可根據(jù)自己需求添加任意多的新類(lèi)型堰燎,而無(wú)需改變 tune() 方法。

class Instrument {
  void play(Note n) { print("Instrument.play() " + n); }
  String what() { return "Instrument"; }
  void adjust() { print("Adjusting Instrument"); }
}

class Wind extends Instrument {
  void play(Note n) { print("Wind.play() " + n); }
  String what() { return "Wind"; }
  void adjust() { print("Adjusting Wind"); }
}   

class Percussion extends Instrument {
  void play(Note n) { print("Percussion.play() " + n); }
  String what() { return "Percussion"; }
  void adjust() { print("Adjusting Percussion"); }
}

class Stringed extends Instrument {
  void play(Note n) { print("Stringed.play() " + n); }
  String what() { return "Stringed"; }
  void adjust() { print("Adjusting Stringed"); }
}

class Brass extends Wind {
  void play(Note n) { print("Brass.play() " + n); }
  void adjust() { print("Adjusting Brass"); }
}

class Woodwind extends Wind {
  void play(Note n) { print("Woodwind.play() " + n); }
  String what() { return "Woodwind"; }
}   

public class Music3 {
  // Doesn't care about type, so new types
  // added to the system still work right:
  public static void tune(Instrument i) {
    // ...
    i.play(Note.MIDDLE_C);
  }
  public static void tuneAll(Instrument[] e) {
    for(Instrument i : e)
      tune(i);
  } 
  public static void main(String[] args) {
    // Upcasting during addition to the array:
    Instrument[] orchestra = {
      new Wind(),
      new Percussion(),
      new Stringed(),
      new Brass(),
      new Woodwind()
    };
    tuneAll(orchestra);
  }
} /* Output:
Wind.play() MIDDLE_C
Percussion.play() MIDDLE_C
Stringed.play() MIDDLE_C
Brass.play() MIDDLE_C
Woodwind.play() MIDDLE_C
*/

在 main 方法中笋轨,我們將引用至于 orchestra 數(shù)組中秆剪,會(huì)自動(dòng)轉(zhuǎn)型到 Instrument赊淑。通過(guò)多態(tài)機(jī)制,tune() 方法完全忽略了它周?chē)a的全部變化仅讽,依舊運(yùn)行正常陶缺。話句話說(shuō),多態(tài)使得程序員能夠 “將改變的事物和未變的事物分離開(kāi)來(lái)”洁灵。

2.4 多態(tài)的適用范圍

只有普通的方法調(diào)用可以是多態(tài)的饱岸,靜態(tài)方法/域 不具有多態(tài)性。

  1. 域訪問(wèn)操作會(huì)由編譯器解析徽千,因此不是多態(tài)的

    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 FieldAccess {
      public static void main(String[] args) {
        Super sup = new Sub(); // Upcast
        System.out.println("sup.field = " + sup.field +
          ", sup.getField() = " + sup.getField());
        Sub sub = new Sub();
        System.out.println("sub.field = " +
          sub.field + ", sub.getField() = " +
          sub.getField() +
          ", sub.getSuperField() = " +
          sub.getSuperField());
      }
    } /* Output:
       * sup.field = 0, sup.getField() = 1
       * sub.field = 1, sub.getField() = 1, sub.getSuperField() = 0
    */
    

    上面的例子中苫费,Sub 包含兩個(gè) field(本身的 和 從父類(lèi)Super 繼承來(lái)的),如果需要得到父類(lèi)的的 field双抽,必須顯式指明 Super.field百框。

  2. 靜態(tài)方法的行為不具有多態(tài)性 -- 靜態(tài)方法是與類(lèi),而非單個(gè)對(duì)象相關(guān)聯(lián)的

    class StaticSuper {
      public static String staticGet() {
        return "Base staticGet()";
      }
      public String dynamicGet() {
        return "Base dynamicGet()";
      }
    }
    
    class StaticSub extends StaticSuper {
      public static String staticGet() {
        return "Derived staticGet()";
      }
      public String dynamicGet() {
        return "Derived dynamicGet()";
      }
    }
    
    public class StaticPolymorphism {
      public static void main(String[] args) {
        StaticSuper sup = new StaticSub(); // Upcast
        System.out.println(sup.staticGet());
        System.out.println(sup.dynamicGet());
      }
    } /* Output:
    Base staticGet()
    Derived dynamicGet()
    */
    

3. 構(gòu)造器和多態(tài)

構(gòu)造器是個(gè)很特殊的方法荠诬,盡管構(gòu)造器不具備多態(tài)性[1]琅翻,但還是有必要理解構(gòu)造器怎樣通過(guò)多態(tài)[2]在復(fù)雜的層次結(jié)構(gòu)中運(yùn)作。

1.構(gòu)造器實(shí)際上是 static 方法柑贞,隱式聲明了 static方椎,因此不支持多態(tài)。
2.指構(gòu)造器內(nèi)部調(diào)用的方法钧嘶,而非構(gòu)造器本身棠众。

3.1 再再論構(gòu)造器的調(diào)用順序

關(guān)于構(gòu)造器的調(diào)用順序在第五章進(jìn)行了簡(jiǎn)要說(shuō)明(最基本),并在第七章再論(加入繼承)有决, 下一節(jié)就再結(jié)合 多態(tài) 來(lái)進(jìn)一步補(bǔ)充闸拿,本節(jié)先來(lái)做一下回顧并討論該順序的意義所在。

Java 之路 (五) -- 初始化和清理(構(gòu)造器與初始化书幕、方法重載新荤、this、垃圾回收器台汇、枚舉類(lèi)型)
Java 之路 (七) -- 復(fù)用類(lèi)(組合苛骨、繼承、代理苟呐、向上轉(zhuǎn)型痒芝、final、再談初始化和類(lèi)的加載)

給出以下例子:

class Meal {
  Meal() { print("Meal()"); }
}

class Bread {
  Bread() { print("Bread()"); }
}

class Cheese {
  Cheese() { print("Cheese()"); }
}

class Lettuce {
  Lettuce() { print("Lettuce()"); }
}

class Lunch extends Meal {
  Lunch() { print("Lunch()"); }
}

class PortableLunch extends Lunch {
  PortableLunch() { print("PortableLunch()");}
}

public class Sandwich extends PortableLunch {
  private Bread b = new Bread();
  private Cheese c = new Cheese();
  private Lettuce l = new Lettuce();
  public Sandwich() { print("Sandwich()"); }
  public static void main(String[] args) {
    new Sandwich();
  }
} /* Output:
   * Meal()
   * Lunch()
   * PortableLunch()
   * Bread()
   * Cheese()
   * Lettuce()
   * Sandwich()
   */

從結(jié)果中我們可以看出這調(diào)用構(gòu)造器遵循一下的順序:

  1. 調(diào)用基類(lèi)構(gòu)造器
  2. 調(diào)用成員的初始化方法
  3. 調(diào)用導(dǎo)出類(lèi)構(gòu)造器的

這一過(guò)程會(huì)反復(fù)遞歸牵素,首先構(gòu)造根基類(lèi)严衬,然后是下一層導(dǎo)出類(lèi),等等笆呆,知道最底層的導(dǎo)出類(lèi)请琳。

3.1.1 基類(lèi)的構(gòu)造器總是在導(dǎo)出類(lèi)的構(gòu)造器中被調(diào)用

如標(biāo)題所言粱挡,這么做的意義何在

首先構(gòu)造器擔(dān)負(fù)著檢查對(duì)象是否被正確構(gòu)造的任務(wù)单起,同時(shí)導(dǎo)出類(lèi)只能訪問(wèn)自己的成員抱怔,而不能訪問(wèn)基類(lèi)的成員(基類(lèi)成員通常為 private)劣坊,這就導(dǎo)致了只有基類(lèi)的構(gòu)造器能夠?qū)ψ约旱脑剡M(jìn)行初始化嘀倒。因此,必須令所有構(gòu)造器都得到調(diào)用局冰,否則不可能正確構(gòu)造完整對(duì)象测蘑。

這也是為什么編譯器強(qiáng)制每個(gè)導(dǎo)出類(lèi)構(gòu)造器都必須調(diào)用基類(lèi)構(gòu)造器的原因。

如果導(dǎo)出類(lèi)沒(méi)有明確指定調(diào)用基類(lèi)構(gòu)造器康二,就會(huì)自動(dòng)調(diào)用默認(rèn)構(gòu)造器碳胳;如果不存在默認(rèn)構(gòu)造器,編譯器會(huì)報(bào)錯(cuò)沫勿。

其次挨约,當(dāng)進(jìn)行繼承時(shí),我們獲取了基類(lèi)的一切产雹,并可以訪問(wèn)基類(lèi)中 public 和 protected 的成員诫惭。這就意味著導(dǎo)出類(lèi)中,必須假定基類(lèi)的所有成員都有效蔓挖。通常做法就是在構(gòu)造器內(nèi)部確保所要使用的成員都構(gòu)建完畢夕土,因此,唯一的辦法就是首先調(diào)用基類(lèi)構(gòu)造器瘟判,這樣在進(jìn)入導(dǎo)出類(lèi)構(gòu)造器時(shí)怨绣,積累中可供我們?cè)L問(wèn)的成員就都已被初始化。

3.1.2 清理順序

如果某一子對(duì)象依賴(lài)于其他對(duì)象拷获,銷(xiāo)毀的順序應(yīng)該和初始化順序相反

  1. 對(duì)于類(lèi)的成員篮撑,意味著與聲明的順序相反,因?yàn)槌蓡T的初始化是按照聲明的順序進(jìn)行的
  2. 對(duì)于基類(lèi)匆瓜,應(yīng)該首先銷(xiāo)毀其導(dǎo)出類(lèi)赢笨,然后才是基類(lèi)。這是因?yàn)閷?dǎo)出類(lèi)的清理可能會(huì)調(diào)用基類(lèi)的某些方法陕壹,所以需要使基類(lèi)中的構(gòu)建仍起作用质欲。

關(guān)于上述第2條,進(jìn)行補(bǔ)充說(shuō)明:

例子:假定父類(lèi) A 的清理方法為 clean()糠馆,用來(lái)清理 A 中的數(shù)據(jù)嘶伟,然后子類(lèi) B 繼承 A,由于繼承的原因又碌,需要重寫(xiě) clean() 方法九昧,用來(lái)清理 B 中的數(shù)據(jù)绊袋。此時(shí),我們需要 B.clean() 方法中調(diào)用 super.clean()铸鹰。這樣在清理子類(lèi)時(shí)癌别,也會(huì)清理父類(lèi)。反之蹋笼,如果沒(méi)有調(diào)用父類(lèi)的 clean() 方法展姐,父類(lèi)的清理動(dòng)作就不會(huì) 發(fā)生。

將上面的例子一般化:子類(lèi)覆蓋父類(lèi)的某個(gè)方法后(比如為 method())剖毯,如果需要調(diào)用父類(lèi)的 method() 方法圾笨,必須顯式調(diào)用 super.method()。

method() -> 子類(lèi).method()
super.method() -> 父類(lèi).method()

3.2 構(gòu)造器內(nèi)部的多態(tài)方法

雖然前面關(guān)于調(diào)用順序已經(jīng)分析的很清楚了逊谋,但是加入多態(tài)之后擂达,新的問(wèn)題又產(chǎn)生了:如果一個(gè)構(gòu)造器的內(nèi)部 調(diào)用正在構(gòu)建的對(duì)象的某個(gè)動(dòng)態(tài)綁定方法,會(huì)發(fā)生什么情況胶滋?

由于動(dòng)態(tài)綁定的調(diào)用在運(yùn)行時(shí)才決定板鬓,因此對(duì)象無(wú)法知道它是屬于方法所在的那個(gè)類(lèi),還是屬于其導(dǎo)出類(lèi)究恤。如果要調(diào)用構(gòu)造器內(nèi)部的一個(gè)動(dòng)態(tài)綁定方法俭令,那么就要用到那個(gè)方法的被覆蓋之后的定義。但是被覆蓋的方法在對(duì)象被完全構(gòu)造之前就會(huì)被調(diào)用丁溅,這就會(huì)造成一些錯(cuò)誤唤蔗。

對(duì)以上再進(jìn)行補(bǔ)充:任何構(gòu)造器內(nèi)部,整個(gè)對(duì)象可能只是部分形成窟赏,我們只能保證基類(lèi)對(duì)象已經(jīng)進(jìn)行初始化妓柜。如果構(gòu)造器只是在構(gòu)建對(duì)象過(guò)程中的一個(gè)步驟,并且該對(duì)象所屬的類(lèi)是從這個(gè)構(gòu)造器所屬的類(lèi)導(dǎo)出的涯穷,那么導(dǎo)出部分在當(dāng)前構(gòu)造器被調(diào)用的時(shí)刻仍舊是沒(méi)有被初始化的棍掐。然而,一個(gè)動(dòng)態(tài)綁定的方法調(diào)用卻會(huì)向外深入到繼承層次結(jié)構(gòu)內(nèi)部拷况,它可以調(diào)用導(dǎo)出類(lèi)那里的方法作煌。如果我們?cè)跇?gòu)造器內(nèi)部這樣做,那么就可能會(huì)調(diào)用某個(gè)方法赚瘦,而這個(gè)方法所操作的成員可能還未初始化粟誓,這肯定會(huì)招致災(zāi)難。

上面這段話讀起來(lái)可能很拗口起意,舉個(gè)例子:

class Glyph {
  void draw() { print("Glyph.draw()"); }
  Glyph() {
    print("Glyph() before draw()");
    draw();
    print("Glyph() after draw()");
  }
}   

class RoundGlyph extends Glyph {
  private int radius = 1;
  RoundGlyph(int r) {
    radius = r;
    print("RoundGlyph.RoundGlyph(), radius = " + radius);
  }
  void draw() {
    print("RoundGlyph.draw(), radius = " + radius);
  }
}   

public class PolyConstructors {
  public static void main(String[] args) {
    new RoundGlyph(5);
  }
} /* Output:
Glyph() before draw()
RoundGlyph.draw(), radius = 0
Glyph() after draw()
RoundGlyph.RoundGlyph(), radius = 5
*/

可以看到鹰服,RoundGlyph 覆蓋了 draw() 方法,在 Glyph 的構(gòu)造器中調(diào)用 RoundGlyph.draw() 方法時(shí)發(fā)生了錯(cuò)誤:輸出時(shí) radius 的值不是默認(rèn)初始值 1 ,而是 0悲酷。

Glyph 構(gòu)造器調(diào)用 draw() 方法時(shí)套菜,RoundGlyph 的成員還未進(jìn)行初始化。

實(shí)際上一節(jié)的初始化順序并不完整设易。初始化的實(shí)際過(guò)程是:

  1. 在其他任何事物發(fā)生之前逗柴,將分配給對(duì)象的存儲(chǔ)空間初始化成二進(jìn)制的零。
  2. 如前所述那樣調(diào)用基類(lèi)構(gòu)造器顿肺。此時(shí)戏溺,調(diào)用被覆蓋后的draw()方法(要在調(diào)用RoundGlyph構(gòu)造器之前調(diào)用),由于步驟 1 的緣故挟冠,我們此時(shí)會(huì)發(fā)現(xiàn) radius 的值為 0于购。
  3. 按照聲明的順序調(diào)用成員的初始化方法。
  4. 調(diào)用導(dǎo)出類(lèi)的構(gòu)造器

這樣的好處就是所有東西至少初始化為零知染,而不是僅僅留作垃圾

因此,我們得出以下結(jié)論:構(gòu)造器內(nèi)唯一能夠安全調(diào)用的方法是基類(lèi)中的 final 方法(包含 private 方法斑胜,因?yàn)?private 屬于 final 方法)控淡,這些方法無(wú)法被覆蓋,也就不會(huì)出現(xiàn)上述問(wèn)題止潘。


4. 協(xié)變返回類(lèi)型

協(xié)變返回類(lèi)型指的是:導(dǎo)出類(lèi)中的被覆蓋方法可以返回基類(lèi)方法的返回類(lèi)型的某種導(dǎo)出類(lèi)型掺炭。

換個(gè)說(shuō)法:導(dǎo)出類(lèi) 覆蓋(即重寫(xiě)) 基類(lèi) 方法時(shí),返回的類(lèi)型可以是基類(lèi)方法返回類(lèi)型的子類(lèi)凭戴。

舉個(gè)例子:

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 CovariantReturn {
  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);
  }
} /* Output:
Grain
Wheat
*/

5. 用繼承進(jìn)行設(shè)計(jì)

5.1 再論組合與繼承

進(jìn)行設(shè)計(jì)時(shí)涧狮,首選組合。組合可以動(dòng)態(tài)選擇類(lèi)型(因此也就選擇了行為)么夫;相反者冤,繼承再編譯時(shí)就需要知道確切類(lèi)型。

看一個(gè)例子:

class Actor {
  public void act() {}
}

class HappyActor extends Actor {
  public void act() { print("HappyActor"); }
}

class SadActor extends Actor {
  public void act() { print("SadActor"); }
}

class Stage {
  private Actor actor = new HappyActor();
  public void change() { actor = new SadActor(); }
  public void performPlay() { actor.act(); }
}

public class Transmogrify {
  public static void main(String[] args) {
    Stage stage = new Stage();
    stage.performPlay();
    stage.change();
    stage.performPlay();
  }
} /* Output:
HappyActor
SadActor
*/

設(shè)計(jì)的通用準(zhǔn)則:用繼承表達(dá)行為間的差異档痪,用成員表達(dá)狀態(tài)上的變化涉枫。

上例中,通過(guò)繼承得到兩個(gè)不同的類(lèi)腐螟,用于表達(dá) act() 方法的差異愿汰;而 Stage 通過(guò)組合是自己的狀態(tài)發(fā)生變化。這種情況下乐纸,這種狀態(tài)的改變也就產(chǎn)生了行為的改變衬廷。

5.2 純繼承與擴(kuò)展

純粹的"is-a"關(guān)系:基類(lèi)和導(dǎo)出類(lèi)具有相同的接口

  • 圖示:
  • 也可以認(rèn)為這是一種純替代,即導(dǎo)出類(lèi)可以完全代替基類(lèi)汽绢,基類(lèi)可以接收發(fā)送給導(dǎo)出類(lèi)的任何消息吗跋。
  • 我們只需從導(dǎo)出類(lèi)向上轉(zhuǎn)型,永遠(yuǎn)不要知道正在處理的對(duì)象的確切類(lèi)型庶喜。

擴(kuò)展的"is-like-a"關(guān)系:導(dǎo)出類(lèi)有著和基類(lèi)相同的基本接口小腊,同時(shí)還具有由額外方法實(shí)現(xiàn)的其他特性

  • 是更為有用且明智的方法
  • 問(wèn)題在于救鲤,當(dāng)進(jìn)行向上轉(zhuǎn)型時(shí),不能使用擴(kuò)展部分(基類(lèi)無(wú)法訪問(wèn)導(dǎo)出類(lèi)的擴(kuò)展部分)

5.3 向下轉(zhuǎn)型與運(yùn)行時(shí)類(lèi)型識(shí)別

5.3.1 運(yùn)行時(shí)類(lèi)型識(shí)別

運(yùn)行時(shí)類(lèi)型識(shí)別(RTTI)指的是在運(yùn)行期間對(duì)類(lèi)型進(jìn)行檢查的行為秩冈。

Java 語(yǔ)言中本缠,所有轉(zhuǎn)型都會(huì)在運(yùn)行期時(shí)對(duì)其進(jìn)行檢查,以便保證它的確是我們希望的那種類(lèi)型入问。如果不是丹锹,就會(huì)返回一個(gè) ClassCastException。

RTTI 不僅僅包括轉(zhuǎn)型處理芬失。比如它提供一種方法楣黍,使我們?cè)谠噲D向下轉(zhuǎn)型之前,查看索要處理的類(lèi)型棱烂。

5.3.2 向下轉(zhuǎn)型

由于向上轉(zhuǎn)型會(huì)丟失具體的類(lèi)型信息租漂,所以希望通過(guò)向下轉(zhuǎn)型(在繼承層次中向下移動(dòng))重新獲取類(lèi)型信息。

通過(guò)例子來(lái)講解向下轉(zhuǎn)型的要點(diǎn):

class Fruit {
    void name(){
        System.out.println("Fruit");
    }
}
class Apple extends Fruit{
    @Override
    void name(){
        System.out.println("Apple");
    }
    
    void color(String c){
        System.out.println("This apple's color is " + c);
    }
}
  1. 正確的向下轉(zhuǎn)型:
    先進(jìn)行向上轉(zhuǎn)型颊糜,然后再進(jìn)行向下轉(zhuǎn)型哩治。此時(shí)會(huì)轉(zhuǎn)型成功,可以調(diào)用子類(lèi)的特殊方法衬鱼。

    Fruit a = new Apple();//先向上轉(zhuǎn)型
    a.name();
    
    Apple apple = (Apple)a;//再向下轉(zhuǎn)型业筏,不會(huì)出錯(cuò)(正確的)
    apple.name();
    apple.color("red");
    
    //輸出:
    //Apple
    //Apple
    //This apple's color is red
    
  1. 不安全的向下轉(zhuǎn)型:
    不經(jīng)過(guò)向上轉(zhuǎn)型,直接向下轉(zhuǎn)型鸟赫。此時(shí)編譯不會(huì)報(bào)錯(cuò)蒜胖,但運(yùn)行時(shí)會(huì)拋出ClassCastException 異常

    Fruit f = new Fruit();
    Apple apple = (Apple)f;//此處異常
    

總結(jié)

這一張內(nèi)容看起來(lái)比較多,實(shí)際上都是圍繞多態(tài)展開(kāi)抛蚤。

多態(tài)是一種不能單獨(dú)來(lái)看待的特性台谢,相反它只能作為類(lèi)關(guān)系”全景“的一部分,與其他特性協(xié)同工作霉颠。

另外对碌,面向?qū)ο蟮木幊趟枷脒€需打磨。

就這樣吧蒿偎,共勉朽们。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市诉位,隨后出現(xiàn)的幾起案子骑脱,更是在濱河造成了極大的恐慌,老刑警劉巖苍糠,帶你破解...
    沈念sama閱讀 219,427評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件叁丧,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)拥娄,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,551評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門(mén)蚊锹,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人稚瘾,你說(shuō)我怎么就攤上這事牡昆。” “怎么了摊欠?”我有些...
    開(kāi)封第一講書(shū)人閱讀 165,747評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵丢烘,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我些椒,道長(zhǎng)播瞳,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,939評(píng)論 1 295
  • 正文 為了忘掉前任免糕,我火速辦了婚禮赢乓,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘说墨。我一直安慰自己骏全,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,955評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布尼斧。 她就那樣靜靜地躺著,像睡著了一般试吁。 火紅的嫁衣襯著肌膚如雪棺棵。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 51,737評(píng)論 1 305
  • 那天熄捍,我揣著相機(jī)與錄音烛恤,去河邊找鬼。 笑死余耽,一個(gè)胖子當(dāng)著我的面吹牛缚柏,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播碟贾,決...
    沈念sama閱讀 40,448評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼币喧,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了袱耽?” 一聲冷哼從身側(cè)響起杀餐,我...
    開(kāi)封第一講書(shū)人閱讀 39,352評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎朱巨,沒(méi)想到半個(gè)月后史翘,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,834評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,992評(píng)論 3 338
  • 正文 我和宋清朗相戀三年琼讽,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了必峰。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,133評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡钻蹬,死狀恐怖吼蚁,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情脉让,我是刑警寧澤桂敛,帶...
    沈念sama閱讀 35,815評(píng)論 5 346
  • 正文 年R本政府宣布,位于F島的核電站溅潜,受9級(jí)特大地震影響术唬,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜滚澜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,477評(píng)論 3 331
  • 文/蒙蒙 一粗仓、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧设捐,春花似錦借浊、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,022評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至槐沼,卻和暖如春曙蒸,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背岗钩。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,147評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工纽窟, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人兼吓。 一個(gè)月前我還...
    沈念sama閱讀 48,398評(píng)論 3 373
  • 正文 我出身青樓臂港,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親视搏。 傳聞我的和親對(duì)象是個(gè)殘疾皇子审孽,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,077評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容

  • 這是16年5月份編輯的一份比較雜亂適合自己觀看的學(xué)習(xí)記錄文檔,今天18年5月份再次想寫(xiě)文章凶朗,發(fā)現(xiàn)簡(jiǎn)書(shū)還為我保存起的...
    Jenaral閱讀 2,762評(píng)論 2 9
  • Swift1> Swift和OC的區(qū)別1.1> Swift沒(méi)有地址/指針的概念1.2> 泛型1.3> 類(lèi)型嚴(yán)謹(jǐn) 對(duì)...
    cosWriter閱讀 11,105評(píng)論 1 32
  • 今天早上醒來(lái)后刷微博杂数,看到一個(gè)營(yíng)銷(xiāo)大號(hào)轉(zhuǎn)發(fā)了一個(gè)微商廣告然后配了一段話:萬(wàn)物皆上漲,唯有臉下垂瘸洛,你還在心疼買(mǎi)護(hù)膚品...
    麗妃娘娘閱讀 3,680評(píng)論 0 2
  • 記得在我小學(xué)三四年級(jí)的時(shí)候就很喜歡畫(huà)畫(huà)揍移,甚至夢(mèng)想著長(zhǎng)大后能成為一名畫(huà)家。后來(lái)媽媽給我報(bào)了個(gè)美術(shù)興趣班反肋,小小的我開(kāi)始...
    金子的小確幸閱讀 327評(píng)論 2 1
  • 收拾起丟落滿(mǎn)地的行囊问畅,曾思考初心是否依然乏苦,卻忘記為什么出發(fā),該去向何處养距。早知道長(zhǎng)不過(guò)執(zhí)念诉探,短不過(guò)善變,終已看透習(xí)慣...
    憶留無(wú)閱讀 571評(píng)論 8 9