一段全都是蛋的簡易Java故事化教學例範程式碼

插圖:Alfredobear

因為聽到某熊聊到,在線上程式課程中學到陣列 (Array),而且把它形容成「儲蛋格」(egg storage tray),覺得很有趣,因此瞬間就給了我一個靈感,來寫一小段Java物件與串流的教學範例程式。

類別 (class) 是絕大多數物件導向程式語言 (Object-Oriented Programming Languages) 的基本單元,在物件導向的理論中,class代表了我們所要描述/解決的問題體系中某一實體或概念的抽象結果。

換句話說,因為我們不能把真實的蛋放進數位世界裡(就算做得到也變成另外一回事了),因此針對我們要解決的問題,把蛋「抽象化」(通常是抽取我們要的數據、狀態或是特徵),再加上一些和蛋有關的行為或是處理工作,就成了所謂的class的基本內容,基本的框架是長這樣:

class Egg {
  // data field, status, characteristics...
  // behaviors, processing tasks...
}

那麼想到陣列,也就是儲蛋格(因為太囉唆以下改稱蛋架egg rack),就會想到創建一個集合型態List,裡面專門放蛋,像是這樣:
List<Egg> eggRack;
從這裡故事就開始了,我開始導入了串流界面相關的功能了……(聽到這會眼神死的人且慢)
首先,空的蛋架很無聊,又沒有蛋可以吃,身為蛋食愛好者當然是無法接受,因此我們需要一個無限供應蛋的串流(輸送帶),源頭裝上一個專門供蛋的供應器 (Supplier<Egg>)。但大部份時候無限供蛋也可以釀成災難,所以我們需要指定數量限制,例如一百顆就好(!),然後在末端裝上一個集蛋器,這樣就可以做為裝滿蛋的蛋架:
eggRack = Stream.generate(Egg::new)  // 無限供蛋器
   .limit(100)                       // 限制數量以免釀災
   .collect(Collectors.toList());    // 集蛋器

這裡Egg::new其實就是Supplier格式:
() -> new Egg() 
的簡寫,稱做method reference,因為傳入值(無)和建構子要求的參數值(也是無)吻合,一直重覆書寫十分累贅,因此可以直接省略兩造,用雙冒號串接,這裡的new是用於建構式的特別寫法。這種語法十分簡潔,是我很喜歡的寫法。

然後呢,想像有一隻很愛吃蛋的熊,他就問囉:「好多蛋呀,這些蛋可以吃嗎?」
於是就一顆一顆從蛋架拿出來吃看看:
eggRack.forEach(Egg::eat);  // 吃吃吃吃吃...

因為我們還沒有針對蛋定義細節,所以就想來玩它一玩,增加一點吃到它的難度:
class Egg{
  boolean wrapped;
  boolean rotten;
  boolean cooked;
}

這裡我定義了蛋的三個狀態:

wrapped(是不是還包裝在保護層裡未解開?)、
rotten(是不是壞掉了?)、
cooked(是不是煮熟了?);

當然最後一定要是熟的才能吃。

接下來想順便導入一下例外處理(好像開始玩太大了)。當我們要吃蛋的時候,依據上面定義的狀態值,有可能拋出兩種例外:
EggStillWrappedException //「蛋還在包裝裡是要怎麼吃」例外
EggNotCookedException    //「蛋沒煮過是要怎麼吃」例外
(為簡單起見例外都做成RuntimeException,也就是不處理也可以編譯並執行)

於是「吃」這件事就定義成這樣了:
void eat() {
  if (wrapped) throw new EggStillWrappedException();  // 如果還在包裝裡...
  if (!cooked) throw new EggNotCookedException();     // 如果還沒煮過...
  System.out.print("Yummi! ");              // 可以吃了!
}

這裡有點好笑的是,其實站在蛋的角度是被吃的,不過為了簡化起見,就先寫在蛋裡好了。接下來的問題是,產生一顆蛋的時候,初始狀態要怎麼設定?
為了娛樂效果,我們把所有的蛋的設成:有包裝、有些許機率會是臭蛋、全都還是生的。因此建構子 (constructor) 就長成這樣了:
//constructor
Egg() { 
  this.wrapped = true;
  this.rotten = Math.random()<0.2;  // 有20%的機率會是臭蛋(!)
  this.cooked = false;
}

於是當熊急著想吃蛋時(執行前面那行狂eat的程式碼),會發生如下的例外:
Exception in thread "main".... EggStillWrappedException 
//「蛋還在包裝裡是要怎麼吃」例外

那麼當然我們需要把蛋解包囉,於是我們為蛋加上解包的程式:
void unwrap() {
  this.wrapped = false; 
  System.out.print("Unwrapped! ");
}

因為要熟了才能吃,所以也要加上烹煮的程式:
void cook(){
  this.cooked = true;
  System.out.print("Cooked! ");
}

不過等一下,蛋如果臭掉拿來煮應該會是個災難吧?所以需要加上判斷,當然也會丟出例外:
void cook(){
   if (this.rotten) throw new CookingRottenEggException(); //「蛋臭了是要怎麼煮」例外
   this.cooked = true;
   System.out.print("Cooked! ");
}

這麼一來,我們就可以依序地把蛋一步一步處理下去(設計對白:某熊問「到底可不可以吃了?」):
eggRack.forEach(Egg::unwrap);
eggRack.forEach(Egg::cook);
eggRack.forEach(Egg::eat);

正興沖沖等著要吃,沒想到又發生例外了。
Exception in thread "main".... CookingRottenEggException
//「蛋臭了是要怎麼煮」例外

原來前面提到20%的臭蛋還沒有處理。

這時候,我們要用串流(輸送帶)把整個流程串起來,好在中間加上過濾器,把臭蛋去掉。
eggRack.stream()
  .peek(Egg::unwrap)
  .filter(not(Egg::isRotten))
  .peek(Egg::cook)
  .forEach(Egg::eat);

peek是一個串流的中繼站,裡面放一個和前面提的Supplier相反的東西,叫做Consumer,意思就是「接收參數但不需要回傳任何東西」,所以也不會影響這個輸送帶上的物件繼續前往下一站。Consumer的長相是這樣的
(Egg egg)->egg.unwrap()
這裡同樣可以使用method reference縮寫,只是它是另一種形式:傳入是單一物件(這裡是egg)、而後面執行的是這個物件「無傳入值」的方法(這裡是unwrap),符合這些條件的,就可以省掉重覆出現的變數egg,改以物件型別加上方法串成:
Egg::unwrap
後面的isRotten(按標準的getter慣例寫成的方法)、cook等都是一樣的道理。而filter()的功能是經過它時條件符合的才會繼續往下一站去,這裡用到另一種函式介面Predicate,它的特色是針對傳入的物件回傳一個boolean值,讓filter知道該不該放行。因為它是Predicate而不是boolean本身,因此要得到相反的結果(也就是我們希望濾出「沒有」壞掉的蛋)無法直接使用邏輯否定運算子(!,這裡真的是寫成驚嘆號),必須使用Function界面提供的not()方法來反轉語意,寫成:
not(Egg::isRotten)
經過以上的努力,某熊終於可以大快朵頤一番了。程式執行結果例:

Unwrapped! Unwrapped! Cooked! Yummi! Unwrapped! Cooked! Yummi! Unwrapped! Cooked! Yummi! Unwrapped! Unwrapped! Cooked! Yummi! Unwrapped! Cooked! Yummi! Unwrapped! Cooked! Yummi! Unwrapped! Cooked! Yummi! Unwrapped! Unwrapped! Cooked! Yummi! Unwrapped! Cooked! Yummi! Unwrapped! Cooked! Yummi! Unwrapped! Cooked! Yummi! Unwrapped! Cooked! Yummi! Unwrapped! Cooked! Yummi! Unwrapped! Unwrapped! Cooked! Yummi! Unwrapped! Cooked! Yummi! Unwrapped! Cooked! Yummi! Unwrapped! Cooked! Yummi! Unwrapped! Cooked! Yummi! Unwrapped! Cooked! Yummi! Unwrapped! Unwrapped! Unwrapped! Cooked! Yummi! Unwrapped! Cooked! Yummi! Unwrapped! Cooked! Yummi! Unwrapped! Cooked! Yummi! Unwrapped! Cooked! Yummi! Unwrapped! Cooked! Yummi! Unwrapped! Cooked! Yummi! Unwrapped! Unwrapped! Unwrapped! Cooked! Yummi! Unwrapped! Cooked! Yummi! Unwrapped! Cooked! Yummi! Unwrapped! Unwrapped! Cooked! Yummi! Unwrapped! Cooked! Yummi! Unwrapped! Cooked! Yummi! Unwrapped! Cooked! Yummi! Unwrapped! Cooked! Yummi! Unwrapped! Unwrapped! Cooked! Yummi! Unwrapped! Unwrapped! Cooked! Yummi! Unwrapped! Cooked! Yummi! Unwrapped! Cooked! Yummi! Unwrapped! Unwrapped! Unwrapped! Unwrapped! Cooked! Yummi! Unwrapped! Cooked! Yummi! Unwrapped! Cooked! Yummi! Unwrapped! Cooked! Yummi! Unwrapped! Cooked! Yummi! Unwrapped! Cooked! Yummi! Unwrapped! Cooked! Yummi! Unwrapped! Cooked! Yummi! Unwrapped! Cooked! Yummi! Unwrapped! Cooked! Yummi! Unwrapped! Cooked! Yummi! Unwrapped! Cooked! Yummi! Unwrapped! Unwrapped! Cooked! Yummi! Unwrapped! Cooked! Yummi! Unwrapped! Cooked! Yummi! Unwrapped! Cooked! Yummi! Unwrapped! Cooked! Yummi! Unwrapped! Cooked! Yummi! Unwrapped! Cooked! Yummi! Unwrapped! Cooked! Yummi! Unwrapped! Cooked! Yummi! Unwrapped! Cooked! Yummi! Unwrapped! Cooked! Yummi! Unwrapped! Cooked! Yummi! Unwrapped! Cooked! Yummi! Unwrapped! Cooked! Yummi! Unwrapped! Cooked! Yummi! Unwrapped! Unwrapped! Cooked! Yummi! Unwrapped! Cooked! Yummi! Unwrapped! Unwrapped! Cooked! Yummi! Unwrapped! Unwrapped! Cooked! Yummi! Unwrapped! Cooked! Yummi! Unwrapped! Cooked! Yummi! Unwrapped! Cooked! Yummi! Unwrapped! Cooked! Yummi! Unwrapped! Unwrapped! Cooked! Yummi! Unwrapped! Cooked! Yummi! Unwrapped! Cooked! Yummi! Unwrapped! Cooked! Yummi! Unwrapped! Unwrapped! Cooked! Yummi! Unwrapped! Cooked! Yummi! Unwrapped! Cooked! Yummi! 

留言

這個網誌中的熱門文章

病毒肆虐下、資料集間國名比對的聯想

關於「複合形式」(摘錄自作品首演文件)

以圖形資料建構Star Wars星戰宇宙中的行星們