第八章 多态
- 多種類型(從同一基類導出的)視為同一類型來處理
- 同一份代碼也就可以毫無差别地運作在這些不同類型之上了
8.1 再論向上轉型
- 對象可以作為自己本身使用,也可以作為它的基類型使用
- 把對某個對象的引用視為對基類型的引用的做法被稱作向上轉型
8.1.1 忘記對象類型public class Car { public void move() { System.out.println("嘟嘟嘟~隻要是個車就能跑的嘟嘟嘟"); } } public class Jeep extends Car{ @Override public void move() { System.out.println("嘟嘟嘟~直接上山了"); } } public class Person { public void drive(Car car) { car.move(); } public static void main(String[] args) { // Jeep繼承自Car,不需要任何轉換,可以運作 Jeep jeep = new Jeep(); drive(jeep); } }
練習1、建立一個Cycle類,它具有子類Unicycle、Bicycle、Tricycle,示範它們都可以經由iride()方法向上轉型為Cycle
- 如果讓drive(Car car)方法接受Jeep的引用,看起來更直覺
- 但是如果我新添加了若幹種車,比如:寶馬、奔馳、奧迪、比亞迪等等
- 那麼還得再為它們寫對應的drive方法,那麼就需要更多的程式設計,做大量的工作
public class Cycle { } public class Unicycle extends Cycle { } public class Bicycle extends Cycle { } public class Tricycle extends Cycle { } public class E01_Upcasting { public static void ride(Cycle c) {} public static void main(String[] args) { ride(new Cycle()); // No upcasting ride(new Unicycle()); // Upcast ride(new Bicycle()); // Upcast ride(new Tricycle()); // Upcast } }
8.2 轉機
8.2.1 方法調用綁定
- 編譯器如何知道這個Car引用指向的就是Jeep對象,而不是奧迪、寶馬等對象呢,其實編譯器無法得知
8.2.2 産生正确的行為
- 将一個方法調用同一個方法主體關聯起來被稱作綁定
- 程式執行前進行綁定,叫做前期綁定
- 程式運作時進行綁定,叫做後期綁定,在對象中安置了某種資訊
- Java中除了static和final方法(包括private)以外所有方法都是後期綁定
- 将某個方法聲明為final,可以防止其他人覆寫該方法,“關閉”動态綁定,生成更有效的代碼
- 大多數情況下并不會對程式的性能有什麼提升,是以最好是根據設計而不是性能來使用final
// 向上轉型可以這麼簡單 Car car = new Jeep(); // 如果調用一個基類的方法,可能認為是調用的父類對象,實際上是正确的調用了Jeep.move(); car.move();
Car基類為所有的導出類都建立了一個公共接口,所有車都可以移動,導出類通過覆寫這些定義,為每種不同的車型提供單獨的move行為
練習2、在幾何圖形的示例中添加@Override注解
略
練習3、在基類中添加一個新方法,導出類不覆寫,其中一個覆寫,最後都覆寫,看看發生了什麼
// 車的父類 public class Car { public void move() { System.out.println("是個車子就能動彈"); } } // 人 public class Person { public static void main(String[] args) { Audi audi = new Audi(); Jeep jeep = new Jeep(); audi.move(); jeep.move(); } } ———————————————————————————————————————————————————————————— // 奧迪不覆寫move方法 public class Audi extends Car { // @Override // public void move() { // System.out.println("小奧迪,嗖嗖快"); // } } // Jeep不覆寫move方法 public class Jeep extends Car { // @Override // public void move() { // System.out.println("大Jeep,直接跑上山"); // } } // 運作結果 是個車子就能動彈 是個車子就能動彈 —————————————————————————————————————————————————————————————— // 奧迪覆寫move方法 public class Audi extends Car { @Override public void move() { System.out.println("小奧迪,嗖嗖快"); } } // Jeep中依舊不覆寫 public class Jeep extends Car { // @Override // public void move() { // System.out.println("大Jeep,直接跑上山"); // } } // 運作結果 小奧迪,嗖嗖快 是個車子就能動彈 ——————————————————————————————————————————————————————————————— // 奧迪覆寫move方法 public class Audi extends Car { @Override public void move() { System.out.println("小奧迪,嗖嗖快"); } } // Jeep覆寫move方法 public class Jeep extends Car { @Override public void move() { System.out.println("大Jeep,直接跑上山"); } } // 運作結果 小奧迪,嗖嗖快 大Jeep,直接跑上山 ——————————————————————————————————————————————————————————————— 結論: 如果不覆寫也可以調,調的是基類的方法 如果覆寫了,調的就是覆寫後的方法
練習4、向Shape.java中添加一個新的Shape類型,并在main()方法中驗證,多态對于新類型的作用是否和舊類型中的一樣
略
練習5、以練習1為基礎,在Cycle中添加wheels()方法,傳回輪子的數量,修改ride()方法,調用wheels()方法,證明多态起作用了
8.2.3 可擴充性public class Cycle { public int wheels() { return 0; } } public class Unicycle extends Cycle { public int wheels() { return 1; } } public class Bicycle extends Cycle { public int wheels() { return 2; } } public class Tricycle extends Cycle { public int wheels() { return 3; } } public class E01_Upcasting { public static void ride(Cycle c) { System.out.println("車輪子數為:" + c.wheels()); } public static void main(String[] args) { ride(new Cycle()); // No upcasting ride(new Unicycle()); // Upcast ride(new Bicycle()); // Upcast ride(new Tricycle()); // Upcast } } // 運作結果 車輪子數為:0 車輪子數為:1 車輪子數為:2 車輪子數為:3
- 如果上面的Car的例子中添加一個drift()漂移的方法,我們添加新的方法并不會影響drive()方法去調用move()方法
- 我們所做的代碼修改,不會對程式中其他不應受到影響的部分産生破壞
- 多态讓程式員将“改變的事物與未變的事物分離開來”
練習6、修改Music3.java,使what()方法成為根Object的toString()方法,并列印出Instrument對象
證明了每一個對象調用了它們自己相應的toString()方法
練習7、向Music3.java添加一個新的類型Instrument,并驗證多态性是否作用于所添加的新類型
毫無疑問,肯定作用于新類型
練習8、修改Music3.java,使其可以像Shape.Java中的方法那樣随機建立Instrument對象
略
練習9、建立Rodent:老鼠,鼹鼠,大頰鼠等等這樣一個繼承結構
略了,和上文提到的Car的例子一樣
練習10、建立一個包含兩個方法的基類,第一個方法中可以調用第二個方法,然後産生一個繼承自該基類的導出類,且覆寫基類中的第二個方法,為該導出類建立一個對象,将它向上轉型到基類并調用第一個方法,解釋發生的情況
8.2.4 缺陷:“覆寫”私有方法public class Car { public void move() { System.out.println("是個車子就能動彈"); brokeDown(); } public void brokeDown() { System.out.println("車抛錨了,尼瑪币車胎炸了!!!"); } } public class Jeep extends Car { @Override public void brokeDown() { System.out.println("大Jeep上山被大石頭給幹廢了!"); } } public class Person { public static void main(String[] args) { Jeep jeep = new Jeep(); jeep.move(); } } // 運作結果 是個車子就能動彈 大Jeep上山被大石頭給幹廢了! // 這個例子我要專門寫一下,因為我當初并不了解是如何調用的 對于 Car car = new Jeep(); car.move(); 我可以了解,一定是調用的Jeep.move()方法 而對于現在這個例子,一開始我是捉摸不透的,以為move()中調用的還是基類的brokeDown()方法, 但是明顯結果并不是,結果是調用的子類的方法 ——————————————————————————————————————————————————————————————————— 當時是這麼一個場景,BaseListFragment類中的上拉加載中調用了appendData()方法, 但是基類中的appendData()并不能滿足需求,需要被重寫,我當時并不了解這些基礎, 有一個疑問:父類的這個上拉加載會不會調用我重寫的appendData()方法呢?是以我陷入了困境, 當時的想法竟然是不行那我就在實作類中再寫一個上拉加載的監聽,然後調用我重寫的appendData(), 那麼這兩個方法都是在同一個類中的方法就肯定可以調用了, 那麼當時的想法真是幼稚,簡直是沒事給自己多加負擔,Java還需要你寫這麼複雜嗎? Java總是使用派生最多的方法作為對象類型 說白了就是父類中各種調用方法,會使用你目前對象所能感覺到的最新的覆寫過的方法
- 基類中的private方法無法被覆寫,在子類中,對于基類中的private方法,最好采用不同的名字
8.2.5 缺陷:域或靜态方法public class Car { private void turnOnTheLight() { System.out.println("把燈開啦"); } } public class Audi extends Car { public void turnOnTheLight() { System.out.println("小奧迪打開了個好看的大燈"); } } ———————————————————————————————————————————————————————————————————— // 聲明為基類的引用 public class Person { public static void main(String[] args) { Car car = new Audi(); // eclipse直接報錯,不能這麼寫 car.turnOnTheLight(); } } // 運作結果 按書中說的結果應該是去執行基類的方法,輸出:把燈開啦 但是我用eclipse編譯直接報錯,無法運作,提示你必須更改private通路權限 ——————————————————————————————————————————————————————————————————— 聲明為子類的引用 public class Person { public static void main(String[] args) { Audi car = new Audi(); car.turnOnTheLight(); } } // 運作結果 執行了子類的方法: 小奧迪打開了個好看的大燈
- 域不是多态的,和方法是不一樣的,舉個例子
public class Car { public int price; public int getPrice() { return price; } } public class Audi extends Car { public int price = 300; public int getPrice() { return price; } } public class Person { public static void main(String[] args) { Car car = new Audi(); System.out.println("car.price=" + car.price); System.out.println("car.getPrice=" + car.getPrice()); Audi audi = new Audi(); System.out.println("audi.price=" + audi.price); System.out.println("audi.getPrice=" + audi.getPrice()); } } // 運作結果 car.price=0 car.getPrice=300 audi.price=300 audi.getPrice=300 // 如果是聲明父類的引用,會調用父類的變量,由此可見域是沒有多态的,方法有多态 // 如果是直接聲明子類的引用,那麼跟父類就沒什麼關系了 // 這裡還有一個要注意的就是,聲明父類的引用,就不能直接調用子類新添加的方法了,如果實在想調,就強制轉型
- 靜态方法也不具有多态性
- 靜态屬性,靜态方法,和非靜态屬性都不具有多态性
- 簡單說,域和靜态方法都不具有多态性
public class Car { public static void fly() { System.out.println("車子能不能飛,得看你是啥車"); } public void move() { System.out.println("是個車子就能動彈"); } } public class Audi extends Car { public static void fly() { System.out.println("我這個奧迪車,好像夠嗆能飛"); } @Override public void move() { System.out.println("小奧迪跑的嗖嗖的"); } } public class Person { public static void main(String[] args) { Car car = new Audi(); car.fly(); car.move(); Audi audi = new Audi(); audi.fly(); car.move(); } } // 運作結果 車子能不能飛,得看你是啥車 小奧迪跑的嗖嗖的 我這個奧迪車,好像夠嗆能飛 小奧迪跑的嗖嗖的 // 可以看出來靜态方法沒有多态,靜态方法是與類,而非與單個對象綁定的
8.3 構造器和多态
8.3.1 構造器調用順序
- 基類的構造器總是在導出類的構造過程中被調用,按照繼承層次逐漸向上連結
- 如果沒有明确指定調用某個基類的構造器,它就會默默地調用預設構造器
class Bread { Bread() { print("Bread()"); } } class Cheese { Cheese() { print("Cheese()"); } } class Lettuce { Lettuce() { print("Lettuce()"); } } class Meal { Meal() { print("Meal()"); } } class Lunch extends Meal { Lunch() { print("Lunch()"); } } class PortableLunch extends Lunch { PortableLunch() { print("PortableLunch()"); } } class Sandwich extends PortableLunch { private Bread b = new Bread(); private Cheese c = new Cheese(); private Lettuce l = new Lettuce(); private Sandwich() { print("Sandwich()"); } public static void main(String[] args) { new Sandwich(); } } // 運作結果 Meal() Lunch(); PortableLunch(); Bread(); Cheese(); Lettuce(); Sandwich(); // 這題剛看的時候突然忘了繼承這一回事了,想想這章講的多态,也是醉了
練習11、向Sandwish().java中添加Pickle類
沒啥說的,直接看上面的例子吧
8.3.2 繼承與清理
- 銷毀的順序應該與初始化的順序相反
練習12、修改練習9,使其能夠示範基類和導出類的初始化順序,然後向基類和導出類中添加成員對象,說明建構起見的初始化順序
略
練習13、在ReferenceCounting.java中添加一個finalize()方法,用來校驗終止條件
略
練習14、修改練習12,使得其某個成員變為具有引用計數的共享對象,并證明它可以正确運作
略
8.3.3 構造器内部的多态方法的行為
- 如果構造器的内部調用正在構造的對象的某個動态綁定方法
- 比如或基類的構造器中調用一個被覆寫的方法
- 被覆寫的方法在對象完全構造之前就被調用,可能會造成一些難于發現的隐藏錯誤
class Glyph { Glyph() { print("Glyph() before draw()"); draw(); print("Glyph() after draw()"); } void draw( ) { print("Glyph.draw()"); } } class RoundGlyph extends Glyph { private int radius = 1; public RoundGlyph(int r) { radius = r; print("RoundGlyph(),radius = " + radius); } void draw() { print("RoundGlyph.draw(),radius = " + radius); } } public class PolyConstructors { public static void main(String[] args) { new RoundGlyph(5); } } // 運作結果 Glyph() before draw() RoundGlyph.draw(),radius = 0 Glyph() after draw() RoundGlyph(),radies = 5; // 這個結果導緻了對RoundGlyph的調用,看起來似乎是我們的目的, // 但是輸出結果并不正确,出現了bug
- 在其他任何事物發生之前,将配置設定給對象的存儲空間初始化成二進制的零
- 由于步驟1的緣故,我們發現radius的值為0
- 按照聲明的順序調用成員的初始化方法
- 調出導出類的構造器主體
盡可能的用簡單的方法使對象進入正常狀态
避免調用其他方法,能安全調用的是final方法(private方法)
練習15、在PolyConstructors.java中添加一個RectangularGlyph,并證明會出現本節所描述的問題
略
8.4 協變傳回類型
這個書中說的有點繞,大體意思就是class Shop { Audi buyCar() { return new Audi(); } } Shop shop = new Shop(); Car car = shop.buyCar(); // Java SE5之前的版本必須傳回Car的對象,盡管Audi是Car的子類也不允許傳回 // 那麼現在可以了,正常shop.buyCar()傳回了一個Audi對象
8.5 用繼承進行設計
組合更加靈活,優先選擇組合
練習16、遵循Transmogrify,java這個例子,建立一個Starship類,包含一個AlertStatus引用,此引用可以訓示三種不同的狀态,納入一些可以改變這些狀态的方法
8.5.1 純繼承與擴充class AlertStatus { public String getStatus() { return "None"; } } class RedAlertStatus extends AlertStatus { public String getStatus() { return "Red"; }; } class YellowAlertStatus extends AlertStatus { public String getStatus() { return "Yellow"; }; } class GreenAlertStatus extends AlertStatus { public String getStatus() { return "Green"; }; } class Starship { private AlertStatus status = new GreenAlertStatus(); public void setStatus(AlertStatus istatus) { status = istatus; } public String toString() { return status.getStatus(); } } public class E16_Starship { public static void main(String args[]) { Starship eprise = new Starship(); System.out.println(eprise); eprise.setStatus(new YellowAlertStatus()); System.out.println(eprise); eprise.setStatus(new RedAlertStatus()); System.out.println(eprise); } } // 運作結果 Green Yellow Red // 完全可以展現出盡量用組合的觀點
8.5.2 向下轉型
- 純繼承就是完全和基類一樣,是一個(is-a)的關系
- 擴充就是在基類的基礎上增加額外資訊,像一個(like-a)的關系
- 擴充導緻擴充部分不能被基類所通路
練習17、使用練習1中的Cycle的層次結構,在Unicycle和Bicycle中添加balance()方法,而Tricycle中不添加,建立所有這三種類型的執行個體,并将它們向上轉型為Cycle數組,數組的每一個元素上都嘗試調用balance(),并觀察結果,然後将它們向下轉型,再次調用balance(),并觀察将發生什麼public class Car { public void move() { System.out.println("是個車子就能動彈"); } } public class Audi extends Car { @Override public void move() { System.out.println("小奧迪跑的嗖嗖的"); } public static void fly() { System.out.println("我這個奧迪車,好像夠嗆能飛"); } } public class Person { public static void main(String[] args) { Car car = new Car(); Car audi = new Audi(); // 轉型失敗,傳回一個ClassCastException異常 ((Audi)car).fly(); // 轉型成功 ((Audi)audi).fly(); } } // 如果是聲明父類的引用,建立子類的執行個體,那麼可以向下轉型 // 如果聲明父類的引用,建立了父類的執行個體,那麼就是一個父類的對象,無法向下轉型
public class E17_RTTI { public static void main(String[] args) { Cycle[] cycles = new Cycle[]{ new Unicycle(), new Bicycle(), new Tricycle() }; // Compile time: method not found in Cycle: // cycles[0].balance(); // cycles[1].balance(); // cycles[2].balance(); ((Unicycle)cycles[0]).balance(); // Downcast/RTTI ((Bicycle)cycles[1]).balance(); // Downcast/RTTI ((Unicycle)cycles[2]).balance(); // Exception thrown } } public class Unicycle extends Cycle { public void balance() {} } public class Bicycle extends Cycle { public void balance() {} } ———————————————————————————————————————————————————————————————————— // 上面是官方答案,我再用我寫的車子寫個例子 // 車的基類 public class Car { } // 奧迪 public class Audi extends Car { public void move() { System.out.println("小奧迪跑的嗖嗖的"); } } // Jeep public class Jeep extends Car { public void move() { System.out.println("大Jeep直接開上山"); } } // 寶馬 public class Bmw extends Car { } public class Person { public static void main(String[] args) { Car[] cars = new Car[] {new Audi(),new Jeep(),new Bmw()}; // cars[0].move(); // cars[1].move(); // cars[2].move(); ((Audi)cars[0]).move(); ((Jeep)cars[1]).move(); ((Jeep)cars[2]).move(); } } // 運作結果分析 如果直接調用,因為是聲明的父類的引用,根本就找不到move()方法 cars[0]和cars[1]轉為Audi和Jeep沒問題 但是cars[2]本身是Bmw的對象,盡管聲明為父類的引用,但是想要轉成Jeep,那指定不可以
8.6 總結
多态意味着“不同的形式”,多态可以帶來很多的成效,更快的程式開發過程、更好的代碼組織、更好擴充的程式以及更容易的代碼維護等