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);
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。
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();
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;
          }
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 中的介面,因為也是用繼承語
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,所以第二行編譯失敗!
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) {
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 解決需求變化
        相信你一定常聽人家說,寫程式要有彈性,要有可維護性!那麼什麼叫有彈性?
      何謂可維護?老實說,這是有點抽象的問題,這邊從最簡單的定義開始:如果增加新
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 的行為。
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("  平甚
                                           平甚
                                           平甚
                                           平甚"));
          }
      }
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
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);
          }
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);
          }
      }
        這一連串的修改,都是為了調整程式架構,這只是個簡單的示範,想像一下,在
      更大規模的程式中調整程架構會有多麼麻煩,而且不只是修改程式很麻煩,沒有被修
      改到的地方,也有可能因此出錯:
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;
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 的特性,介面還有一些細
節必須明瞭,像是介面中的預設語法、介面中定義常數之運用、匿名內部類別之撰寫
等,這都將於本節中詳細說明。

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 的特性,介面還有一些細 節必須明瞭,像是介面中的預設語法、介面中定義常數之運用、匿名內部類別之撰寫 等,這都將於本節中詳細說明。