介面定義行為


老闆今天想開發一個海洋樂園遊戲,當中所有東西都會游泳。你想了一下,談到會游的東西,第一個想到的就是魚,之前剛學過繼承,也知道繼承可以運用多型,你也許會定義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是一種FishShark是一種FishPiranha是一種Fish,如果你讓Human繼承Fish,那Human是一種Fish?你會說「美人魚啊!」...@#\$%^&

程式上可以通過編譯也可以執行,但邏輯上或設計上有不合理的地方,你可以繼續硬掰下去,如果現在老闆說加個潛水航呢?寫個Submarine繼承Fish嗎?Submarine是一種Fish嗎?繼續這樣的想法設計下去,你的程式架構會越來越不合理,越來越沒有彈性!

記得嗎?Java中只能繼承一個父類別,所以更強化了「是一種」關係的限制性。如果今天老闆突發奇想,想把海洋樂園變為海空樂園,有的東西會游泳,有的東西會飛,有的東西會游也會飛,如果用繼承方式來解決,寫個Fish讓會游的東西繼承,寫個Bird讓會飛的東西繼承,那會游也會飛的怎麼辦?有辦法定義個飛魚FlyingFish同時繼承FishBird嗎?

重新想一下需求吧!老闆今天想開發一個海洋樂園遊戲,當中所有東西都會游泳。「所有東西」都會「游泳」,而不是「某種東西」都會「游泳」,先前的設計方式只解決了「所有魚」都會「游泳」,只要它是一種魚(也就是繼承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並不知道每條魚怎麼游,所以使用第二種處理方式。

目前AnemonefishSharkPiranha繼承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的語意來說,繼承會有「是一種」關係,實作介面則表示「擁有行為」,但不會有「是一種」的關係。HumanSubmarine實作了Swimmer,所以都擁有Swimmer定義的行為,但它們沒有繼承Fish,所以它們不是一種魚,這樣的架構比較合理也較有彈性,可以應付一定程度的需求變化。

有些書或文件會說,HumanSubmarine是一種Swimmer,會有這種說法的作者,應該是有C++程式語言的背景,因為C++中可以多重繼承,也就是子類別可以擁有兩個以上的父類別,若其中一個父類別用來定義為抽象行為,該父類別的作用就類似Java中的介面,因為也是用繼承語意來實作,所以才會有是一種的說法。

多重繼承容易因為設計上考量不周而引來不少麻煩,因而Java對多重繼承作了限制,就類別的語意來說,Java中限制只能繼承一個父類別,所以「是一種」的語意更為強烈,我建議將「是一種」的語意保留給繼承,對於介面實作則使用「擁有行為」的語意,如此就不會搞不清楚類別繼承與介面實作的差別,對於何時用繼承,何時用介面也比較容易判斷。

廣義來說,Java中的介面確實是支援多重繼承的一種方式,不過在JDK8之前,Java的介面只能定義抽象方法,不能有任何方法實作,這也是Java對多重繼承作限制的表現,但也引來設計上的一些不便之處。為了支援Lambda新特性的引進,從JDK8開始,Java的介面也放寬了一些限制,介面中也可以有條件地進行方法實作,這在之後介紹Lambda時會再討論。