虛有其表的介面


在Java中,你可以定義「介面」,也就是使用關鍵字interface來定義一個物件所應擁有的「行為」。這種說法與定義類別時定義行為有點交集,因為定義類別時,也會定義行為,在某些場合,也會稱定義類別時,那些可操作的行為是物件的介面,或者說是物件與物件間溝通的「協定」(Protocol)

確實地,定義類別時,那些類別上所展開的行為也是物件的介面,定義類別時,可以選擇是否實作介面中的細節點,或更具體的,實作方法的本體內容,也可以選擇不實作,這時該方面會是個抽象方法,該類別也就是個抽象類別了。

如果你使用interface關鍵字來定義物件的介面,你就真的只是在定義介面,而不會涉及介面的實作,這是與定義類別上的介面最大的不同。也就是你只是在定義行為,行為如何展開並不會定義,或更具體地,只定義方法而不實作方法本體內容。

無疑地,使用interface定義物件的介面時,該介面一定就是抽象的,因為你所抽取事物的表象僅僅是行為,而沒有具體的動作方式,只不過就語法上來說,並不需要在interface關鍵字前加上abstract,也不用在介面中的方法加上abstract。

如果你繼承某個抽象類別,則必然有「是一種」(is-a)的關係,這是決定是否使用繼承時一個非常重要的考量依據。例如你有一個鳥(Bird)類別,你選擇繼承它來實現鴿子(Pigeon),則鴿子必然是一種鳥。

如果你實作一個介面,則不會有「是一種」的關係(就Java的語法而言),雖然有些書或文件,會用一種類型(a type of )來說明這種情況,不過我比較傾向於用「有此種類型的行為」來解釋。舉個例子來說,鳥會飛,超人也會飛,但超人不會是一種鳥(如果你硬要說鳥人也是超人,那我投降),如果你只是為了飛行這個行為,讓超人去繼承鳥就不適當,也許你可以定義一個具有飛行這個行為的介面叫飛行者(Flyer)。
public interface Flyer {
    void fly();
}

如果鳥實作飛行者:
public class Bird implements Flyer {
    public void fly() {
        ...
    }
}

則我會說,鳥具有飛行者的行為,這會比說鳥是一種飛行者來的清楚。同樣地,如果超人實作飛行者:
public class Superman implements Flyer {
    public void fly() {
        ...
    }
}

則我會說,超人具有飛行者的行為,這會比說超人是一種飛行者來的清楚。超人不會是一種鳥,鳥也不會是一種超人。

像C ++這種支援多重繼承的語言,實現以上的機制,是靠繼承來達成,子類別會繼承某個類別的實作,再繼承另一個類別的抽象行為,後者的作用此時就像Java的 interface,因此鳥「是一種」飛行者,或超人「是一種」飛行者的說法也是正確,更適用於解釋像C++的這種多重繼承方式。

但Java不支援多重繼承,要達成上述的結果,是靠interface定義與實現,採用鳥具有飛行者的行為、超人具有飛行者的行為,會比較能更清楚區分何時使用類別,何時使用介面的依據。

Java中,類別可以實作多個介面的行為,例如有的飛機,也許同時可以飛行與在水上滑行:
public class Seaplane implements Flyer, Sailor {
    ...
}

這時稱水上飛機擁有飛行者的行為,也擁有航行者(Sailor)的行為。依你的需求而定,也許你的水上飛機像飛機多一些,則你可以決定是否繼承飛機已經有的許多實作(飛機已實作飛行者的行為):
public class Seaplane extends Airplane implements Sailor {
    ...
}

由於類別可以實現多個介面,你可能會有疑問的是,如果A介面與B介面都定義了some()行為,而某類別同時實作了A與B介面,這樣是否會有衝突?答案是不會,可以通過編譯器,執行上也不會有問題,不過你要思考的是,為什麼A介面與B介面都定義了some()行為,它們是代表相同的行為嗎?如果是的話,你應該思考的是,定義一個父介面P,將some()定義在P介面中:
public interface P {
    void some();
}

然後A與B介面繼承P所定義的行為:
public interface A extends P {
    ... 其它行為定義
}

public interface B extends P {
    ... 其它行為定義
}

這叫作行為的繼承。在Java中,介面可以繼承自多個介面,也就是可以繼承多個角色的行為,再增加自己的行為定義。

在上例中,如果A介面與B介面的some()行為,其實表現方式並不一樣,則此時你要思考的,是區別兩個介面中的行為名稱,不要都叫some(),避免名稱衝突。

介面中可以定義常數,例如:
public interface Some {
    public final static int CA = 1;
    ...
}

public interface Other {
    public final static int CA = 1;
    ...
}

如果你同時實作這兩個介面,則編譯時並不會報錯:
public class Impl implements Some, Other {
}

但如果你試圖如下存取:
System.out.println(Impl.CA);

則編譯器會不清楚你打算使用的是Some.CA或是Other.CA而發生編譯錯誤:
Impl.java:4: reference to CA is ambiguous, both variable CA in Some and variable
 CA in Other match
        System.out.println(Impl.CA);

事實上,存取介面上所定義常數正確的作法,是透過介面名稱而非實作類別名稱,若你明確指定是存取哪個介面中的常數,當然就不會有問題:
System.out.println(Some.CA);
System.out.println(Other.CA);