Java SE 7 技術手冊第七章草稿 - 何謂介面?
- 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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 的特性,介面還有一些細
節必須明瞭,像是介面中的預設語法、介面中定義常數之運用、匿名內部類別之撰寫
等,這都將於本節中詳細說明。