老闆今天想開發一個海洋樂園遊戲,當中所有東西都會游泳。你想了一下,談到會游的東西,第一個想到的就是魚,之前剛學過繼承,也知道繼承可以運用多型,你也許會定義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);
}
@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
?你會說「美人魚啊!」...@#\$%^&
程式上可以通過編譯也可以執行,但邏輯上或設計上有不合理的地方,你可以繼續硬掰下去,如果現在老闆說加個潛水航呢?寫個Submarine
繼承Fish
嗎?Submarine
是一種Fish
嗎?繼續這樣的想法設計下去,你的程式架構會越來越不合理,越來越沒有彈性!
記得嗎?Java中只能繼承一個父類別,所以更強化了「是一種」關係的限制性。如果今天老闆突發奇想,想把海洋樂園變為海空樂園,有的東西會游泳,有的東西會飛,有的東西會游也會飛,如果用繼承方式來解決,寫個Fish
讓會游的東西繼承,寫個Bird
讓會飛的東西繼承,那會游也會飛的怎麼辦?有辦法定義個飛魚FlyingFish
同時繼承Fish
跟Bird
嗎?
重新想一下需求吧!老闆今天想開發一個海洋樂園遊戲,當中所有東西都會游泳。「所有東西」都會「游泳」,而不是「某種東西」都會「游泳」,先前的設計方式只解決了「所有魚」都會「游泳」,只要它是一種魚(也就是繼承Fish
)。
「所有東西」都會「游泳」,代表了「游泳」這個「行為」可以被所有東西擁有,而不是「某種」東西專屬,對於「定義行為」,在Java中可以使用interface
關鍵字定義:
package cc.openhome;
public interface Swimmer {
public abstract void swim();
}
以下程式碼定義了Swimmer
介面,介面可用於定義行為,但不定義實作,在這邊Swimmer
中的swim()
方法沒有實作,直接標示為abstract
,而且一定是public
。物件若想擁有Swimmer
定義的行為,就必須實作Swimmer
介面。例如Fish
擁有Swimmer
行為:
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();
}
類別要實作介面,必須使用implements
關鍵字,實作某介面時,對介面中定義的方法有兩種處理方式,一是實作介面中定義的方法,二是再度將該方法標示為abstract
。在這個範例子中,由於Fish
並不知道每條魚怎麼游,所以使用第二種處理方式。
目前Anemonefish
、Shark
與Piranha
繼承Fish
後的程式碼如同先前示範的片段,無需修改。那麼,如果Human
要能游泳呢?
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
不是一種Fish
。類似地,Submarine
也有Swimmer
的行為:
package cc.openhome;
public class Submarine implements Swimmer {
private String name;
public Submarine(String name) {
this.name = name;
}
public String getName() {
return name;
}
@Override
public void swim() {
System.out.printf("潛水艇 %s 潛行%n", name);
}
}
Submarine
實作了Swimmer
,不過Submarine
沒有繼承Fish
,所以Submarine
不是一種Fish
。
以Java的語意來說,繼承會有「是一種」關係,實作介面則表示「擁有行為」,但不會有「是一種」的關係。Human
與Submarine
實作了Swimmer
,所以都擁有Swimmer
定義的行為,但它們沒有繼承Fish
,所以它們不是一種魚,這樣的架構比較合理也較有彈性,可以應付一定程度的需求變化。
有些書或文件會說,Human
與Submarine
是一種Swimmer
,會有這種說法的作者,應該是有C++程式語言的背景,因為C++中可以多重繼承,也就是子類別可以擁有兩個以上的父類別,若其中一個父類別用來定義為抽象行為,該父類別的作用就類似Java中的介面,因為也是用繼承語意來實作,所以才會有是一種的說法。
多重繼承容易因為設計上考量不周而引來不少麻煩,因而Java對多重繼承作了限制,就類別的語意來說,Java中限制只能繼承一個父類別,所以「是一種」的語意更為強烈,我建議將「是一種」的語意保留給繼承,對於介面實作則使用「擁有行為」的語意,如此就不會搞不清楚類別繼承與介面實作的差別,對於何時用繼承,何時用介面也比較容易判斷。
廣義來說,Java中的介面確實是支援多重繼承的一種方式,不過在JDK8之前,Java的介面只能定義抽象方法,不能有任何方法實作,這也是Java對多重繼承作限制的表現,但也引來設計上的一些不便之處。為了支援Lambda新特性的引進,從JDK8開始,Java的介面也放寬了一些限制,介面中也可以有條件地進行方法實作,這在之後介紹Lambda時會再討論。