Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.

Java SE 7 技術手冊第七章草稿 - 何謂介面?

2,649 views

Published on

Java SE 7 技術手冊

Published in: Technology
  • Be the first to comment

  • Be the first to like this

Java SE 7 技術手冊第七章草稿 - 何謂介面?

  1. 1. Java SE 7 稿 草冊手術技 何謂介面?7.1 何謂介面? 第 6 章談過繼承,但你也許在一些書或文件中看過「別濫用繼承」 ,或者「優先考慮介面而不是用繼承」的說法,什麼情況叫濫用繼承?介面代表的又是什麼?這一節將實際來看個海洋樂園遊戲,探討看看如何設計與改進,瞭解繼承、介面與多型的用與不用之處。7.1.1 介面定義行為 老闆今天想開發一個海洋樂園遊戲,當中所有東西都會游泳。你想了一下,談到會游的東西,第一個想到的就是魚,上一章剛學過繼承,也知道繼承可以運用多型,你也許會定義 Fish 類別中有個 swim()的行為:public abstract class Fish { protected String name; public Fish(String name) { this.name = name; } public String getName() { return name; } public abstract void swim();} 由於實際上每種魚游泳方式不同 所以將 swim()定義為 abstract, , 因此 Fish也是 abstract。接著定義小丑魚繼承魚:public class Anemonefish extends Fish { public Anemonefish(String name) { super(name); } @Override public void swim() { System.out.printf(" 魚丑小 %s 泳游 %n", name); }} Anemonefish 繼承了 Fish 並實作 swim()方法 也許你還定義了鯊魚 Shark , ,類別繼承 Fish、食人魚 Piranha 繼承 Fish:public class Shark extends Fish { public Shark(String name) { super(name);
  2. 2. Java SE 7 稿 草冊手術技 } @Override public void swim() { System.out.printf(" 魚鯊 %s 泳游 %n", name); }}public class Piranha extends Fish { public Piranha(String name) { super(name); } @Override public void swim() { System.out.printf(" 魚人食 %s 泳游 %n", name); }} 老闆說話了 為什麼都是魚?人也會游泳啊!怎麼沒寫?於是你就再定義 Human ,類別繼承 Fish...等一下!Human 繼承 Fish? 不會覺得很奇怪嗎?你會說程式沒錯啊!編譯器也沒抱怨什麼! 對!編譯器是不會抱怨什麼,就目前為止,程式也可以執行,但是請回想上一章 是一種(曾談過,繼承會有是一種(is-a)的關係,所以 Anemonefish 是一種 Fish,Shark 是一種 )是一種 Fish,Piranha 是一種 Fish,如果你讓 Human 繼承 Fish,那 Human 是一種 Fish?你會說「美人魚啊!」...@#$%^& 圖 7.1 繼承會有 is-a 關係 提示 圖 7.1 中 Fish 是斜體字,表示是抽象類別,而 swim()是斜體字,表示 是抽象方法,name 旁邊有個#,表示為 protected。
  3. 3. Java SE 7 稿 草冊手術技 程式上可以通過編譯也可以執行,但邏輯上或設計上有不合理的地方,你可以繼 續硬掰下去,如果現在老闆說加個潛水航呢?寫個 Submarine 繼承 Fish 嗎? Submarine 是一種 Fish 嗎?繼續這樣的想法設計下去,你的程式架構會越來越不 合理,越來越沒有彈性! 記得嗎?Java 中只能繼承一個父類別,所以更強化了「是一種」關係的限制性。 如果今天老闆突發奇想,想把海洋樂園變為海空樂園,有的東西會游泳,有的東西會 飛,有的東西會游也會飛,如果用繼承方式來解決,寫個 Fish 讓會游的東西繼承, 寫 個 Bird 讓 會 飛 的 東 西 繼 承 , 那 會 游 也 會 飛 的 怎 麼 辦 ? 有 辦 法 定 義 個 飛 魚 FlyingFish 同時繼承 Fish 跟 Bird 嗎? 重新想一下需求吧!老闆今天想開發一個海洋樂園遊戲,當中所有東西都會游 泳。 「所有東西」都會「游泳」 ,而不是「某種東西」都會「游泳」 ,先前的設計方式 只解決了「所有魚」都會「游泳」 ,只要它是一種魚(也就是繼承 Fish) 。 「所有東西」都會「游泳」 ,代表了「游泳」這個「行為」可以被所有東西擁有, 而不是「某種」東西專屬,對於「定義行為」 ,在 Java 中可以使用 interface 關鍵Lab 字定義: OceanWorld1 Swimmer.java package cc.openhome; public interface Swimmer { public abstract void swim(); } 以下程式碼定義了 Swimmer 介面,介面僅用於定義行為,所以 Swimmer 中的 swim()方法不能實作,直接標示為 abstract,而且一定是 public。物件若想擁 有 Swimmer 定義的行為,就必須實作 Swimmer 介面。例如 Fish 擁有 Swimmer 行Lab 為: OceanWorld1 Fish.java package cc.openhome; public abstract class Fish implements Swimmer { protected String name; public Fish(String name) { this.name = name; } public String getName() { return name; } @Override public abstract void swim();
  4. 4. Java SE 7 稿 草冊手術技 } 類別要實作介面,必須使用 implements 關鍵字,實作某介面時,對介面中定 義的方法有兩種處理方式,一是實作介面中定義的方法,而是再度將該方法標示為 abstract。在這個範例子中,由於 Fish 並不知道每條魚怎麼游,所以使用第二種 處理方式。 目前 Anemonefish、Shark 與 Piranha 繼承 Fish 後的程式碼如同先前示範Lab 的片段,無需修改。那麼,如果 Human 要能游泳呢? OceanWorld1 Human.java package cc.openhome; public class Human implements Swimmer { private String name; public Human(String name) { this.name = name; } public String getName() { return name; } @Override public void swim() { System.out.printf(" 類人 類人 類人 類人 %s 泳游 泳游 泳游 泳游 %n", name); } } Human 實作了 Swimmer,不過這次 Human 可沒有繼承 Fish,所以 Human 不Lab 是一種 Fish。類似地,Submarine 也有 Swimmer 的行為: OceanWorld1 Submarine.java package cc.openhome; public class Submarine implements Swimmer { private String name; public Submarine(String name) { this.name = name; } public String getName() { return name; }
  5. 5. Java SE 7 稿 草冊手術技 @Override public void swim() { System.out.printf(" 艇水潛 艇水潛 艇水潛 艇水潛 %s 行潛 行潛 行潛 行潛 %n", name); }} Submarine 實 作 了 Swimmer , 不 過 Submarine 沒 有 繼 承 Fish , 所 以Submarine 不是一種 Fish。目前程式的架構如下: 圖 7.3 運用介面改進程式架構 提示 圖 7.3 中的虛線空心箭頭表示實作介面。 的語意來說,繼承會有「是一種」關係,實作介面則表示「擁有行為」 以 Java 的語意來說,繼承會有「是一種」關係,實作介面則表示「擁有行為」 ,但不會有「是一種」的關係。Human 與 Submarine 實作了 Swimmer,所以都擁有但不會有「是一種」的關係Swimmer 定義的行為,但它們沒有繼承 Fish,所以它們不是一種魚,這樣的架構比較合理也較有彈性,可以應付一定程度的需求變化。 提示 有些書或文件會說,Human 與 Submarine 是一種 Swimmer,會有這種 說法的作者,應該是有 C++程式語言的背景,因為 C++中可以多重繼承, 也就是子類別可以擁有兩個以下的父類別,若其中一個父類別用來定義 為抽象行為,該父類別的作用就類似 Java 中的介面,因為也是用繼承語
  6. 6. Java SE 7 稿 草冊手術技 意來實作,所以才會有是一種的說法。 由於 Java 只能繼承一個父類別,所以「是一種」的語意更為強烈,我建 議將「是一種」的語意保留給繼承,對於介面實作則使用「擁有行為」 的語意,如此就不會搞不清楚類別繼承與介面實作的差別,對於何時用 繼承,何時用介面也比較容易判斷。7.1.2 行為的多型 在 6.1.2 曾試著當編譯器 判斷哪些繼承多型語法可以通過編譯 加入扮演 , , (Cast)語法的目的又是為何,以及哪些情況下,執行時期會扮演失敗,哪些又可扮演成功。會使用介面定義行為之後,也要再來當編譯器,看看哪些是合法的多型語法。例如:Swimmer swimmer1 = new Shark();Swimmer swimmer2 = new Human();Swimmer swimmer3 = new Submarine(); 這三行程式碼都可以通過編譯,判斷方式是「右邊是不是擁有左邊的行為」 ,或「是右邊物件是不是實作了左邊介面」 。 是否擁有行為?是否實作介面? 圖 7.4 是否擁有行為?是否實作介面? Shark 擁有 Swimmer 行為嗎?有的!因為 Fish 實作了 Swimmer 介面,也就是 Fish 擁有 Swimmer 行為,Shark 繼承 Fish,當然也擁有 Swimmer 行為,所以通過編譯,Human 與 Submarine 也都實作了 Swimmer 介面,所以通過編譯。 更進一步地,來看看底下的程式碼是否可通過編譯?Swimmer swimmer = new Shark();Shark shark = swimmer; 第一行要判斷 Shark 是否擁有 Swimmer 行為?是的!可通過編譯,但第二行呢?swimmer 是 Swimmer 型態,編譯器看到該行會想到,有 Swimmer 行為的物件是不是 Shark 呢?這可不一定!也許實際上是 Human 實例!因為有 Swimmer 行為的物件不一定是 Shark,所以第二行編譯失敗!
  7. 7. Java SE 7 稿 草冊手術技 就上面的程式碼片段而言,實際上 swimmer 是參考至 Shark 實例,你可以加上扮演(Cast)語法:Swimmer swimmer = new Shark();Shark shark = (Shark) swimmer; 對第二行的語意而言,就是在告訴編譯器,對!你知道有 Swimmer 行為的物件,不一定是 Shark,不過你就是要它扮演 Shark,所以編譯器就別再囉嗦了。可以通過編譯,執行時期 swimmer 確實也是參考 Shark 實例,所以也沒有錯誤。 底下的程式片段會在第二行編譯失敗:Swimmer swimmer = new Shark();Fish fish = swimmer; 第二行 swimmer 是 Swimmer 型態,所以編譯器會問,實作 Swimmer 介面的物件是不是繼承 Fish?不一定,也許是 Submarine!因為會實作 Swimmer 介面的並不一定繼承 Fish,所以編譯失敗了。如果加上扮演語法:Swimmer swimmer = new Shark();Fish fish = (Fish) swimmer; 第二行告訴編譯器,你知道有 Swimmer 行為的物件,不一定繼承 Fish,不過你就是要它扮演 Fish 所以編譯器就別再囉嗦了 可以通過編譯 執行時期 swimmer , 。 ,確實也是參考 Shark 實例,它是一種 Fish,所以也沒有錯誤。 下面這個例子就會拋出 ClassCastException 錯誤:Swimmer swimmer = new Human();Shark shark = (Shark) swimmer; 在第二行 swimmer 實際上參考了 Human 實例 你要他扮演鯊魚?這太荒繆了! , ,所以執行時就出錯了。類似地,底下的例子也會出錯:Swimmer swimmer = new Submarine();Fish fish = (Fish) swimmer; 在第二行,swimmer 實際上參考了 Submarine 實例,你要他扮演魚?又不是在演哆啦 A 夢中海底鬼岩城,哪來的機器魚情節!Submarine 不是一種 Fish,執行時期會因為扮演失敗而拋出 ClassCastException。 知道以下的語法,哪些可以通過編譯,哪些可以扮演成功作什麼?來考慮一個需求,寫個 static 的 swim()方法,讓會游的東西都游起來,在不會使用介面多型語法時,也許你會寫下:public static void doSwim(Fish fish) { fish.swim();}public static void doSwim(Human human) { human.swim();}public static void doSwim(Submarine submarine) {
  8. 8. Java SE 7 稿 草冊手術技 submarine.swim(); } 老實說,如果已經會寫下接收 Fish 的 doSwim()版本,程式還算是不錯的,因 為至少你知道,只要有繼承 Fish,無論是 Anemonefish、Shark 或 Piranha,都 可以使用 Fish 的 doSwim()版本,至少你會使用繼承時的多型,至於 Human 與 Submarine 各使用其接收 Human 及 Submarine 的 doSwim()版本。 問題是,如果「種類」很多怎麼辦?多了水母、海蛇、蟲等種類呢?每個種類重 載一個方法出來嗎?其實在你的設計中,會游泳的東西,都擁有 Swimmer 的行為,Lab 都實作了 Swimmer 介面,所以你只要這麼設計就可以了: OceanWorld2 Ocean.java package cc.openhome; 參數是 Swimmer 型態 public class Ocean { public static void doSwim(Swimmer swimmer) { swimmer.swim(); } public static void main(String[] args) { doSwim(new Anemonefish(" 莫尼 ")); doSwim(new Shark(" 尼蘭 ")); doSwim(new Human(" 汀斯賈 ")); 號一色黃 doSwim(new Submarine(" ")); } } 執行結果如下: 泳 游 莫尼 魚丑小 泳游 尼蘭 魚鯊 泳 游 汀斯賈 類人 行潛 號一 色黃 艇水潛 只要是實作 Swimmer 介面的物件,都可以使用範例中的 doSwim()方法, Anemonefish、Shark、Human、Submarine 等,都實作了 Swimmer 介面,再多 種類,只要物件擁有 Swimmer 行為,你就都不用撰寫新的方法,可維護性顯然提高 許多! 解決需求 需求變化 7.1.3 解決需求變化 相信你一定常聽人家說,寫程式要有彈性,要有可維護性!那麼什麼叫有彈性? 何謂可維護?老實說,這是有點抽象的問題,這邊從最簡單的定義開始:如果增加新
  9. 9. Java SE 7 稿 草冊手術技 的需求,原有的程式無需修改,只需針對新需求撰寫程式,那就是有彈性、具可維護 性的程式。 以 7.1.1 提到的需求為例,如果今天老闆突發奇想,想把海洋樂園變為海空 樂園,有的東西會游泳,有的東西會飛,有的東西會游也會飛,那麼現有的程式可以 應付這個需求嗎? 仔細想想,有的東西會飛,但不限於某種東西才有「飛」這個行為,有了先前的Lab 經驗,你使用 interface 定義了 Flyer 介面: OceanWorld3 Flyer.java package cc.openhome; public interface Flyer { public abstract void fly(); } Flyer 介面定義了 fly()方法,程式中想要飛的東西,可以實作 Flyer 介面, 假設有台海上飛機具有飛行的行為,也可以在海面上航行,可以定義 Seaplane 實Lab 作、Swimmer 與 Flyer 介面: OceanWorld3 Airplane.java package cc.openhome; public class Seaplane implements Swimmer, Flyer { private String name; public Seaplane(String name) { this.name = name; } @Override public void fly() { System.out.printf(" 機飛上海 %s 飛在 %n", name); } @Override public void swim() { System.out.printf(" 機飛上海 %s 面海 行航 %n", name); } } 在 Java 中,類別可以實作兩個以上的類別,也就是擁有兩種以上的行為。例如 類別可以實作兩個以上的類別,也就是擁有兩種以上的行為 類別可以實作兩個以上的類別 Seaplane 就同時擁有 Swimmer 與 Flyer 的行為。
  10. 10. Java SE 7 稿 草冊手術技 如果是會游也會飛的飛魚呢?飛魚是一種魚,可以繼承 Fish 類別,飛魚會飛,Lab 可以實作 Flyer 介面: OceanWorld3 FlyingFish.java package cc.openhome; public class FlyingFish extends Fish implements Flyer { public FlyingFish(String name) { super(name); } @Override public void swim() { System.out.println(" 泳游魚飛 "); } @Override public void fly() { System.out.println(" 飛會魚飛 "); } } 在 類別可以同時繼承某個類別,並實作某些介面。例 正如範例所示,在 Java 中,類別可以同時繼承某個類別,並實作某些介面 如 FlyingFish 是一種魚,也擁有 Flyer 的行為。如果現在要讓所有會游的東西游Lab 泳,那麼 7.1.1 節中的 doSwim()方法就可以滿足需求了,因為 Seaplane 擁有 Swimmer 的行為,而 FlyingFish 也擁有 Swimmer 的行為: OceanWorld3 Ocean.java package cc.openhome; public class Ocean { public static void doSwim(Swimmer swimmer) { swimmer.swim(); } public static void main(String[] args) { 略 ... doSwim(new Seaplane(" 號零軍空 號零軍空 號零軍空 號零軍空 ")); doSwim(new FlyingFish(" 平甚 平甚 平甚 平甚")); } }
  11. 11. Java SE 7 稿 草冊手術技 就滿足目前需求來說,你所作的就是新增程式碼來滿足需求,但沒有修改舊有既 存的程式碼,你的程式確實擁有某種程度的彈性與可維護性。 圖 7.5 海空樂園目前設計架構 海空樂園目前設計架構 設計 當然需求是無止盡的,原有程式架也許確實可滿足某些需求,但有些需求也可能 超過了原有架構預留之彈性,一開始要如何設計才會有彈性,是必須靠經驗與分析判 斷,不用為了保有程式彈性的彈性而過度設計,因為過大的彈性表示過度預測需求, 有的設計也許從不會遇上事先假設的需求。 例如,也許你預先假設會遇上某些需求而設計了一個介面,但從程式開發至生命 週期結束,該介面從未被實作過,或者有一個類別實作過該介面,那麼該介面也許就 不必存在,你事先的假設也許就是過度預測需求。 事先的設計也有可能因為需求不斷增加,而超出原本預留之彈性。例如老闆又開 口了:不是所有的人都會游泳啊!有的飛機只會飛,不能停在海上啊!Lab 好吧!並非所有的人都會游泳,所以不再讓 Human 實作 Swimmer: OceanWorld4 Human.java package cc.openhome; public class Human { protected String name; public Human(String name) { this.name = name; } public String getName() { return name; } }Lab 假設只有游泳選手會游泳,游泳選手是一種人,並擁有 Swimmer 的行為: OceanWorld4 Human.java
  12. 12. Java SE 7 稿 草冊手術技 package cc.openhome; public class SwimPlayer extends Human implements Swimmer { public SwimPlayer(String name) { super(name); } @Override public void swim() { System.out.printf(" 手選泳游 %s 泳游 %n", name); } } 有的飛機只會飛,所以設計一個 Airplane 類別作為 Seaplane 的父類別,Lab Airplane 實作 Flyer 介面: OceanWorld4 Airplane.java package cc.openhome; public class Airplane implements Flyer { protected String name; public Airplane(String name) { this.name = name; } @Override public void fly() { System.out.printf(" 機飛 %s 飛在 %n", name); } } Seaplane 會在海上航行,所以在繼承 Airplane 之後,必須實作 Swimmer 介Lab 面: OceanWorld4 Seaplane.java package cc.openhome; public class Seaplane extends Airplane implements Swimmer { public Seaplane(String name) { super(name); }
  13. 13. Java SE 7 稿 草冊手術技 @Override public void fly() { System.out.print(" 上海 "); super.fly(); } @Override public void swim() { System.out.printf(" 機飛上海 %s 面海 行航 %n", name); } }Lab 不過程式中的直昇機就只會飛: OceanWorld4 Seaplane.java package cc.openhome; public class Helicopter extends Airplane { public Helicopter(String name) { super(name); } @Override public void fly() { System.out.printf(" 機飛 %s 飛在 %n", name); } } 這一連串的修改,都是為了調整程式架構,這只是個簡單的示範,想像一下,在 更大規模的程式中調整程架構會有多麼麻煩,而且不只是修改程式很麻煩,沒有被修 改到的地方,也有可能因此出錯:
  14. 14. Java SE 7 稿 草冊手術技 沒有動到這邊啊!怎麼出錯了? 圖 7.6 沒有動到這邊啊!怎麼出錯了? 程式架構很重要!這邊就是個例子,因為 Human 不在實作 Swimmer 介面了,因 此不能再套用 doSwim()方法!應該改用 SwimPlayer 了! 不好的架構下要修改程式,很容易牽一髮而動全身,想像一下在更複雜的程式 中,修改程式之後,到處出錯的窘境,也有不少人維護到架構不好的程式,抓狂到想 砍掉重練的情況。 提示 對於一些人來說,軟體看不到,摸不著,改程式似乎也不需成本,也因 此架構這東西經常被漠視。曾經聽過一個比喻是這樣的:沒人敢在蓋十 幾層高樓之後,要求修改地下室架構,但軟體業界常常在作這種事。 也許老闆又想到了:水裡的話,將淺海游泳與深海潛行分開好了!就算心裡再千Lab 百個不願意,你還是摸摸鼻子改了: OceanWorld4 Diver.java package cc.openhome; public interface Diver extends Swimmer { public abstract void dive(); } 在 Java 中,介面可以繼承自另一個介面,也就是繼承父介面行為,再於子介面 介面可以繼承自另一個介面,也就是繼承父介面行為, 介面可以繼承自另一個介面Lab 中額外定義行為。假設一般的船可以在淺海航行: 中額外定義行為 OceanWorld4 Boat.java package cc.openhome; public class Boat implements Swimmer { protected String name; public Boat(String name) { this.name = name; } @Override public void swim() { System.out.printf(" 面水在船 %s 行航 %n", name); } }Lab 潛水航是一種船,可以在淺海游泳,也可以在深海潛行: OceanWorld4 Submarine.java package cc.openhome;
  15. 15. Java SE 7 稿 草冊手術技public class Submarine extends Boat implements Diver { public Submarine(String name) { super(name); } @Override public void dive() { System.out.printf(" 艇水潛 %s 行潛 %n", name); }} 需求不斷變化,架構也有可能因此而修改,好的架構在修改時,其實也不會全部的程式碼都被牽動,這就是設計的重要性,不過像這位老闆無止境地在擴張需求,他說一個你改一個,也不是辦法,找個時間,好好跟老闆談談這個程式的需求邊界到底是什麼吧! 你的程式有彈性嗎? 圖 7.7 你的程式有彈性嗎?7.2 介面語法細節 上一節介紹了介面的基礎觀念與語法,然而結合 Java 的特性,介面還有一些細節必須明瞭,像是介面中的預設語法、介面中定義常數之運用、匿名內部類別之撰寫等,這都將於本節中詳細說明。

×