一段全都是蛋的簡易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!
留言