Java 8 初探 - Stream

閱讀時間約 7 分鐘
此為過去的舊文,2014 年 4 月 13 日初次發表於 logdown。

即使沒有 Stream,Java Collection framework 的設計仍是相當不錯,只是有時候需要一些簡單的功能,例如:根據某些條件查找容器中的某個物件,總是要寫個 for 迴圈,程式不難但寫久了也覺得煩。在沒有類似 for in 的語法糖衣之前,index 的管理很惱人。若是巢狀迴圈,index 命名不好遇到問題 debug 起來更是頭痛,用過 Apache Commons Collections 後,Apache Commons Collections 幾乎是專案裡必備的套件。先來個簡單例子,假設想在放 Person 物件的 List 中找姓名含某個特定值時,傳統的寫法如下,會是寫一個 for 迴圈,然後逐一檢查每個person物件的姓和名。

那如果用 Apache Commons Collections 又會如何呢?請看下面的範例,基本上不需要寫 for 迴圈,只要在呼叫 find 時傳一個實作 Predicate 介面的物件即可,該物件只需要實作 evaluate 這個method,判斷是否滿足條件,滿足回傳 true,就這樣。什麼!程式碼行數變多,沒錯,確實變多,但 PersonNamePredicate 這物件是可以重複使用的,若觀察 CollectionUtils 這個 class 就會發現有 12 個 methods 利用 Predicate 物件對容器進行過濾、選擇、計數等操作,加上 Predicate 的實作要測試很容易,所以這樣寫很划算。

好啦!既然主題是 Java 8 新的 Stream API,那用 Stream 該怎麼寫?Stream 的寫法會像下面範例,好像沒有比較省行數,但和原始的 for 迴圈相比,就語意上或是可讀性上,這版本可以解讀成『根據一某條件過濾,然後找第一個,如果結果存在就回傳該物件,不存在就回傳 null』,迴圈的操過被忽略了,是否有感覺抽象程度被提高了呢?

那如果要像 Apache Commons Collections 那樣,可以辦到嗎?可以!首先,像下例,寫個 StreamUtils 輔助類別,提供一個 find(Collection, Predicate) 函式,然後改寫 PersonNamePredicate,接著就可以只寫一行就搞定。當然,如果不打算讓 PersonNamePredicate 同時支援 Apache Commons Collections 及 Java Stream,只需實作 java.util.Predicate 介面的 test 函式就好。

不過繞了一大圈,為的是什麼?除了使用parallelStream()可能帶來平行處理的好處外,這個版本並沒有帶來太多的好處,主要是應用(find)太簡單了,用Stream有點殺雞用牛刀的感覺。

Java Stream API 的概念類似 Unix Pipelinepipes and filters design pattern,透過串接多個簡單的 operation 完成有意義的工作,由於 operation 通常都很簡單,所以使用 Lambda expression 多數時候可以帶來簡潔和提升可讀性的好處。


Figure 1 - Stream Pipeline

Figure 1 - Stream Pipeline


如 Figure 1 所示,Java Stream 能串多個 intermediate operations,但最後只能串一個 terminal operation 來組成 pipeline。用 intermediate operation 轉換 stream 內容,例如:過濾 (filter(Predicate))、替換 (map(Function))、排序 (sorted(Comparator)) 等,然後用 terminal operation 對 stream 內的資料計算最終結果或產生 side effect,例如:收集 (collect(Collector))、逐一改變 (forEach(Consumer)) 或歸納 (Reduce(BinaryOperator)) 等。


Figure 2 - Stream Pipeline Example

Figure 2 - Stream Pipeline Example


例如,可以用 Figure 2 的 pipeline 來計算資料中資產超過 10 億元的富豪,其資產的總合,首先 filter(Predicate) 過濾出資產超過 10 億元的資料,接著用 map(Function) 取出資產的部分,最後用 reduce(BinaryOperation) 做歸納。事實上,類似的運算實在太常用了,因此 Java Stream API 中有個 Collectors 類別提供常用的 terminal operation,例如 summarizingDouble(ToDoubleFunction) 就結合了 map(Function) 和預設的 reduce(BinaryOperation) 實作,簡化 pipeline 的組成。

覺得例子有點抽象?那再來一個更具體的例子吧。假設 Exam 代表一種測驗,每個人可以參加多次測驗,因此 Person 有一個 List 存放受測者參加過的所有測驗。假如想要取得曾經得超過 700 分的所有受測者排名,為了不寫重複的程式碼,如下面的範例程式,先將取得受測者曾經參加過的測驗最高分寫成 PersongetHighestScore() 函式 (仍用Stream API)。

接著就可以寫一個 showRank(List<Person>, double) 的函式,第一個參數是所有受測者,第二個參數是排行榜顯示的門檻。程式首先呼叫 stream() 取得 Stream 物件,接著對 Stream 呼叫 filter(Predicate) 函式,這裡用 Lambda Expression 就覺得很自然,也提高可讀性。

過濾掉低於門檻值的受測者後,呼叫 sorted(Comparator) 進行排序,然後 map(Function) 將受測者的全名與分數組成字串 (例如:"Spirit Tu: 840.0") 當成結果,這邊要小心的是 map(Function) 所回傳的 Stream 物件,裡面裝的已經不是 Person 物件了,而是字串,所以 forEach(Consumer) 的 Lambda Expression 中,e 代表的是字串,直接就可以顯示在 console 上。

最後,呼叫 showRank(persons, 700) 就可以看到曾經得超過 700 分的受測者排行榜了。整個流程可以畫成如 Figure 3的 pipeline。


Figure 3 - The pipeline for showRank

Figure 3 - The pipeline for showRank


老實說,看到 Java Sream API 讓我感到相當親切,這應該跟我研究所多年的研究題目是 visual dataflow language 有關,在 VisualTPL 中,迴圈的概念被內化了,重點在於做什麼運算 (what),而不是如何跑迴圈 (how),同樣地,Java Stream API 也是把迴圈給內化了,每個 operation 的重點是要做什麼,大大提高了程式的抽象化程度和可讀性。不過 Java Stream API 的特色還不只這些,剩下的下一篇再討論。

52會員
102內容數
這是從 Medium 開始的一個專題,主要是想用輕鬆閒談的方式,分享這幾年軟體開發的心得,原本比較侷限於軟體架構,但這幾年的文章不僅限於架構,也聊不少流程相關的心得,所以趁換平台,順勢換成閒談軟體設計。
留言0
查看全部
發表第一個留言支持創作者!
Spirit的沙龍 的其他內容
2024 四國行 Day 5
閱讀時間約 3 分鐘
書摘《程式設計守則》
閱讀時間約 25 分鐘
書摘《從 0 到 1》
閱讀時間約 17 分鐘
書摘《高品質微服務》
閱讀時間約 11 分鐘
Java 8 初探 - Lambda
閱讀時間約 6 分鐘
Java 8 初探 - Closure
閱讀時間約 5 分鐘
你可能也想看
【Java】建立Maven專案使用第三方套件 相信很多初學者學python的原因,不外乎語法簡單、好上手、重點是有很多現成的套件可以玩。那麼,Java呢?有!當然有!而且還多到你不知道該選哪個好! 今天的文章主要示範如何在vscode新建立Java 的maven專案,並且透過maven安裝這些額外的套件(依賴)
Thumbnail
avatar
2023-09-02
【Java】安裝開始、開發環境建立 最近配合公司政策換了新電腦,重新回想起從頭建環境的惡夢。本篇文就來記錄一下如何開始踏入Java的第一步,方便起見也使用相對Eclipse、IntelliJ來說輕量不少的VScode作為編輯器。
Thumbnail
avatar
2023-07-05
Java Script自學經驗回顧來到學期2-3的階段,第一個作業就是打造餐廳清單。原本認為經過電影清單的學習經歷之後,對於打造餐廳清單應該也不會太過困難;沒想到我花了2個月的時間才把作業完整交出去。 在寫餐廳清單的初期,第一個碰到的問題就是首頁無法秀出餐廳評分這個選項。我試著參考其他同學的作品也改了版面的設計,卻始終無法出現餐廳評
avatar
奧莉薇
2023-02-28
Java Bird Coffee|通化街六張犁商圈,小巧而溫馨禾雀咖啡工作室位在通化街尾端,靠近六張犁的禾雀咖啡工作室,質樸的感覺是一家適合假日早晨來待上好一會兒的好去處。 主打自家烘培手沖及義式咖啡,最大特色還有自家製的麵包、甜點輕食,最喜歡吃的莫過於他們的鮮奶吐司。曾經住在這裡好長一段時間,週末去搶個一包可以在家裡烤來吃,是相當幸福的感覺。 冰羅馬咖啡則是我喝過最喜歡的
Thumbnail
avatar
Celine 寫在這裡
2023-02-06
Java abstract class 與 interface 的差別abstract class = 抽象類別 interface = 介面 抽象類別與介面都無法建立物件。 1. 使用abstract關鍵字來建立抽象類別,interface關鍵字建立介面。 interface只能繼承interface,且可以繼承多個:
Thumbnail
avatar
Vic Lin
2022-07-04
JAVA繼承篇繼承 在Java中,一個類可以由其他類派生。如果你要創建一個類,而且已經存在一個類具有你所需要的屬性或方法,那麼你可以將新創建的類繼承該類。 利用繼承的方法,可以重用已存在類的方法和屬性,而不用重寫這些代碼。被繼承的類稱為超類(super class),派生類稱為子類(subclass)。 繼承的特
Thumbnail
avatar
行至水窮處 坐看雲起時
2021-09-19
Java 編譯成 WebAssembly 的工具古早的年代想在網頁內埋 Java 還有 Java applet 可以用,在 Java applet 式微後,找來找去比較可以的辦法大概就是編譯成 WebAssembly 了吧! 想要把 Java 編譯成 WebAssembly,有下面三個工具可以選用
Thumbnail
avatar
Leon
2021-06-22
Java abstract class和interface的使用時機Java abstract class和interface的使用時機
Thumbnail
avatar
Vic Lin
2020-08-20
Java工程師:肉體的交易明細  給對想當Java工程師又興趣的人類,也許你是學生,又或是你正想轉職,我希望你們都不會因為未知而害怕,這條路沒有那麼困難。
Thumbnail
avatar
傑歐葛葛
2020-07-19