# 六角鼠年鐵人賽 Week 17 - Spring Boot - 番外篇 Java 8 Stream Tutorial ==大家好,我是 "為了拿到金角獎盃而努力著" 的文毅青年 - Kai== ## 劇中佳句 讓子彈飛 :::info 世界上本沒有路,有了腿便有了路 ::: ## 主題 承上周介紹了 **Lambda** 後,今天繼續介紹另外一個歷史級的 feature -- **Stream** **Stream** 是 JDK 8 版本提供用來處理 Collections 物件的一套特殊處理機制,同時也是將 Java 原先的迭代處理方式由 **外部迭代**,改為 **內部迭代** 的重要推手。 搭配上 Lambda 表示式,讓開發人員在寫 code 上的發揮如虎添翼,省去了許多繁文縟節,可以更專心地在刻劃業務邏輯上,重複性質的過程盡可能的避免,大大的提升了時程上的效益,也讓 Java 在開發上終於不再被太多的 **前處理** 綁住,逐漸看齊新興高階語言的流暢。 ## 迭代 先補上一個簡單的架構圖,以便清楚明瞭何謂外部迭代、內部迭代。 ![](https://i.imgur.com/zJzLbwy.png) 在介紹 Stream 前,首先要了解的是關於 Java 的迭代,也就是平時工程師在處理 Collection 物件時候會需要用到的 **迴圈**。 > **小知識** 遞迴與迭代? > 迭代演算法 >> 指的是那些經由重複性運算處理,得以讓變數的原值每經過一次運算後能夠得到新值的方法。 >> > 遞迴 >> 是設計與描述演算法的工具,常在使用演算法前被拿出做討論,而能夠採用遞迴的演算法通常具備幾種特質: >> 1. 集合能夠被拆解為許多小集合 >> 2. 小集合的解匯集後可以成為大集合的解 >> 3. 當一個集合無法被拆分成更多的小集合時則視為個體物件,將執行運算流程後返回解,並提供給上一層的集合,以讓其進行運算 >> 4. 每一個集合體(無論大、小、個體物件),都能夠適用於同一種分解和運算流程。 Java 中常見的 **外部迭代** 莫過於 **For Loop**、**For Each**、**While**、**Do While**,而在使用這些 function 的時候,開發人員還是得自己刻畫其邏輯架構、判斷依據、輪迴方式等,其實對於開發人員來說,最重要的莫過於下列的幾點: - 傳入的物件 - 限制條件 - 業務邏輯處理 - 輸出結果 也許有人會說,其實我已經把所有的東西都說出來了不是嗎? 且聽後續娓娓道來,慢慢讓你體會 Stream 的便利之處。 ## Stream 是一種 Collection? Stream 是 Collections Interface 底下的新規格,也就是說所有繼承、實作 Collections 的類別皆有支援。 在上篇 Lambda 表示式的文章內有提到,Java 8 後的版本可以在 Interface 增加 default 的方式,這讓 Stream 可以無痛相容所有 Java 7 的 Collections 類別。 **Collections Interface Content:** ```java= default Stream<E> stream( ) { return StreamSupport.stream(spliterator(), false); } ``` 在應用上,許多我們熟知的 Collections 類別都可以轉成 Stream,舉例: ```java= // List 轉 Stream List.stream() // Set 轉 Stream Set.stream(); // Map 轉 Stream,需要先轉成 Set Map.entrySet().stream(); // Object [] 轉 Stream Arrays.stream(Object[]); ``` 當然反面思考,我們也可以將 Stream 轉為特定的集合物件: ```java= // Stream 轉為 List stream.collect(Collectors.toList()); // Stream 轉為 Set stream.collect(Collectors.toSet()); // Stream 轉為 Map stream.collect(Collectors.toMap(e -> e[0], e -> e[1])); // Stream 轉為 Object [] stream.toArray(size -> new Object[size]); ``` 這種通用性讓我們可以放心的使用 Stream 去處理資料集合,並且能夠把資料集合轉為我們希望的集合型態再做處理。 ## Stream 的邏輯架構 Stream 的方法中各類操作都有其性質,這些性質可分成兩類: - 惰性 (Lazy) - 急性 (Eager) 多數 Stream 的操作都屬於惰性,這表示在進行這些操作時,Stream 並沒有真正的處理資料集合,只是被賦予了許多設定,被告知該如何處理,但不會真正去執行。 要真正讓 Stream 運作起來,必須要在最後設一個急性操作,方能將前面的惰性操作從設定轉為實際處理,這就像是烹飪一道菜,你可以說得滿嘴食材、醬料、調理方式甚至是最後的擺盤等等,但真正推動你下廚房的理由,可能是準備一份晚餐又或者是準備一場約會。 惰性操作種類繁多,通常定義在急性操作前,主要用於**查找**、**分類**、**排序**或**集成Map**等設定,注意!! 這邊都只是設定,必須要在後方搭配一個急性操作,才能將惰性設定轉為實際處理動作,惰性方法舉例如下: | 方法種類 | 方法 | | --- | --- | |**Search Function**|filter, findFirst, findAny, anyMatch, allMatch, distinct| |**Sort Function**|sorted, limit| |**Map Function**|flapMap, Map| 急性操作種類和方法較少,主要進行最後**集成**、**轉型**或**邏輯結果處理**等操作,急性方法舉例如下: | 方法種類 | 方法 | | --- | --- | |**Collect Function**|collect| |**Loop Function**|forEach| |**Logic Function**|if...系列| ## 實際範例 這裡直接展示一個實際的外部迭代和 Stream 版本的內部迭代的差異。 Kai 接下來的範例會建立一個關於歌手和歌曲的物件,並在製成幾個範例物件後裝入 Collection 進行迭代的輸入和輸出。 歌手的物件包含下列資訊: - 歌手名稱 - 歌曲數量 - 曲風 - 專輯主要流行地區 歌曲的物件包含下列資訊: - 歌曲名稱 - 專輯名稱 - 歌曲時間長度 - 以秒為單位 - 作詞者 開始建立歌手和歌曲資訊的物件 **Singer.java** ```java= package kai.com.stream; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.Setter; import lombok.ToString; import java.util.List; @Getter @Setter @AllArgsConstructor @ToString public class Singer { private String name; private int countMusic; private String type; private String country; private List<Song> songs; } ``` **Song.java** ```java= package kai.com.stream; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.Setter; import lombok.ToString; @Getter @Setter @AllArgsConstructor @ToString public class Song { String album; String name; int songSeconds; String lyricist; } ``` > Kai 這邊搭配 Lombok 套件處理建構子、Setter、Getter 和 toString() 部分 > 不熟悉的朋友可以搭配參考 [六角鼠年鐵人賽 Week 11 - Spring Boot - Lombok 省時省力好幫手](/uWHqezBxQ9CPqr5iUOfNFQ?both) 這一篇文章 接著這一單元的主程式 **StreamTest.java** ```java= package kai.com.stream; import java.util.HashMap; import java.util.Map; public class StreamTest { public static void main(String [] args){ // 建立範例 Map Map<Integer, Singer> map = new HashMap<>(); // 建立歌曲物件 Song song_s1_1 = new Song("Beat It","Thriller",259,"Michael Jackson"); Song song_s1_2 = new Song("Billie Jean","Thriller",293,"Michael Jackson"); Song song_s2_1 = new Song("Bad Day","Bad Day",234,"Daniel Powter"); Song song_s2_2 = new Song("Do You Wanna Get Lucky","Holiday Version",218,"Daniel Powter"); Song song_s3_1 = new Song("Blue and White Porcelain","On the Run",243,"Vincent Fang"); // 青花瓷, 我很忙, 方文山 Song song_s3_2 = new Song("Love Confession","Jay Chou's Bedtime Stories",215,"Jay Chou"); // 告白氣球, 周杰倫的床邊故事, 周杰倫 Song song_s4_1 = new Song("UGLY BEAUTY","UGLY BEAUTY",295,"Wu QingFeng"); // 怪美的, 怪美的, 吳青峰 Song song_s4_2 = new Song("The Great Artist","Muse",271,"Matthew Yen"); // 大藝術家, Muse, 嚴云農 Song song_s5_1 = new Song("LIGHT YEARS AWAY","Passengers",235,"G.E.M"); // 光年之外, Passengers, 鄧紫棋 Song song_s5_2 = new Song("City Zoo",null,284,"G.E.M"); // 建立歌手物件並放入歌曲物件 Singer s1 = new Singer("Michael Jackson",100,"All","USA", Arrays.asList(song_s1_1,song_s1_2)); Singer s2 = new Singer("Daniel Powter",20,"Life Romantic","USA",Arrays.asList(song_s2_1,song_s2_2)); Singer s3 = new Singer("Jay Chou",60,"POP Music","Taiwan", Arrays.asList(song_s3_1,song_s3_2)); // 周杰倫 Singer s4 = new Singer("Jolin Tsai",60,"POP Music","Taiwan",Arrays.asList(song_s4_1,song_s4_2)); // 蔡依林 Singer s5 = new Singer("G.E.M",40,"POP Music","China",Arrays.asList(song_s5_1,song_s5_2)); // 鄧紫棋 // 將歌手物件放入 Map map.put(1,s1); map.put(2,s2); map.put(3,s3); map.put(4,s4); map.put(5,s5); } } ``` ### 1. 單純的 Print - 傳統的外部迭代 ```java= for(Map.Entry<Integer,Singer> s:map.entrySet()){ System.out.println(s.toString()); } ``` 輸出成果: ``` Tranditional Iterate: 1=Singer(name=Michael Jackson, countMusic=100, type=All, country=USA) 2=Singer(name=Daniel Powter, countMusic=20, type=Life Romantic, country=USA) 3=Singer(name=Jay Chou, countMusic=60, type=POP Music, country=Taiwan) 4=Singer(name=Jolin Tsai, countMusic=60, type=POP Music, country=Taiwan) 5=Singer(name=G.E.M, countMusic=40, type=POP Music, country=China) ``` - Stream 版的內部迭代 ```java= map.entrySet().stream() .forEach(s -> System.out.println(s.toString())); ``` 輸出成果: ``` Stream Iterate 1=Singer(name=Michael Jackson, countMusic=100, type=All, country=USA) 2=Singer(name=Daniel Powter, countMusic=20, type=Life Romantic, country=USA) 3=Singer(name=Jay Chou, countMusic=60, type=POP Music, country=Taiwan) 4=Singer(name=Jolin Tsai, countMusic=60, type=POP Music, country=Taiwan) 5=Singer(name=G.E.M, countMusic=40, type=POP Music, country=China) ``` 目前來說上述還看不太出來 Stream 的優勢,我們繼續往下做一些分類和處理 ### 2. 篩選 USA 的歌手物件 - 傳統的外部迭代 ```java= for(Map.Entry<Integer,Singer> s:map.entrySet()){ if(s.getValue().getCountry().equals("USA")) System.out.println(s.toString()); } ``` 輸出結果: ``` Tranditional Iterate: 1=Singer(name=Michael Jackson, countMusic=100, type=All, country=USA, songs=[Song(album=Beat It, name=Thriller, songSeconds=259, lyricist=Michael Jackson), Song(album=Billie Jean, name=Thriller, songSeconds=293, lyricist=Michael Jackson)]) 2=Singer(name=Daniel Powter, countMusic=20, type=Life Romantic, country=USA, songs=[Song(album=Bad Day, name=Bad Day, songSeconds=234, lyricist=Daniel Powter), Song(album=Do You Wanna Get Lucky, name=Holiday Version, songSeconds=218, lyricist=Daniel Powter)]) ``` - Stream 版的內部迭代 ```java= map.entrySet().stream() .filter(s -> "USA".equals(s.getValue().getCountry())) .forEach(s -> System.out.println(s.toString())); ``` 輸出結果: ``` Stream Iterate 1=Singer(name=Michael Jackson, countMusic=100, type=All, country=USA, songs=[Song(album=Beat It, name=Thriller, songSeconds=259, lyricist=Michael Jackson), Song(album=Billie Jean, name=Thriller, songSeconds=293, lyricist=Michael Jackson)]) 2=Singer(name=Daniel Powter, countMusic=20, type=Life Romantic, country=USA, songs=[Song(album=Bad Day, name=Bad Day, songSeconds=234, lyricist=Daniel Powter), Song(album=Do You Wanna Get Lucky, name=Holiday Version, songSeconds=218, lyricist=Daniel Powter)]) ``` ### 3. 篩選歌曲秒數在 250 秒以上並列成集合物件 - 傳統的外部迭代 ```java= List<Song> targetSongList = new ArrayList<>(); for(Map.Entry<Integer,Singer> s:map.entrySet()){ List<Song> songList = s.getValue().getSongs(); for(Song song: songList){ if(song.getSongSeconds() > 250) targetSongList.add(song); } } for(Song s:targetSongList){ System.out.println(" - " + s.getName()); } ``` 輸出結果: ``` Tranditional Iterate: - Beat It - Billie Jean - UGLY BEAUTY - The Great Artist - City Zoo ``` - Stream 版的內部迭代 ```java= List<Song> targetSongList2 = map.entrySet().stream() .flatMap(s -> s.getValue().getSongs().stream()) .filter(s -> s.getSongSeconds() > 250) .collect(Collectors.toList()); targetSongList2.stream() .forEach(s -> System.out.println(" - " + s.getName())); ``` 輸出結果: ``` Stream Iterate - Beat It - Billie Jean - UGLY BEAUTY - The Great Artist - City Zoo ``` ### 4. 呈上題,將輸出的歌曲集合作名稱 A - Z 排列 - 傳統的外部迭代 你會發現用傳統的外部迭代方式會很難去對 ArrayList 做排序,你可能需要去實作些特別的判斷式才能完成這件事情。 有興趣可以參考這一篇 [Java School 教 ArrayList 的三種排序方式](http://www.51gjie.com/java/647.html) - Stream 版的內部迭代 ```java= // 以原先的流程來說,只需要多加一行 sorted() 即可處理 targetSongList2.stream() .sorted(Comparator.comparing(Song::getName)) .forEach(s -> System.out.println(" - " + s.getName())); ``` 輸出結果: ``` Stream Iterate - Beat It - Billie Jean - City Zoo - The Great Artist - UGLY BEAUTY ``` ## 統整優劣與結論 從上述範例可以發現到 Stream 的幾個優缺點,例如語法可以較有條理,但是因為無法看到內部迭代的狀況導致 Debug 時候會較以往來的困難等。 但無論如何,Stream 的出現仍然大幅地變動了原先繁雜的 Java 迭代程序,與時俱進的 Java 程式設計師,這是無論如何都必須學會的東西! 統整優缺點如下: |優點 |缺點| |--- |---| |語法清晰較有條理 |須個別了解集合物件內容的資料 | |處理程序能夠一眼看出 |Debug Mode 不好理解迭代中的處理狀況| |省略許多繁雜的重複語法 || |迭代功能齊全|| ## 結語 :::danger 還有許多 Stream 特別的功能 Kai 並沒有全部提到,有興趣的朋友們可以透過 Kai 簡單的介紹後,在實作中一一去接觸~ 目前來說應該只有這兩篇番外是關於 Java 8 變動較大的部分,另外 Kai 還在思考是否寫一篇關於 Optional API 的部分,但相比較前兩篇來說,這個部分算是比較小的變動...。 Anyway,下篇文章便會揭曉XD [六角鼠年鐵人賽 Week 18 - Spring Boot - 番外篇 Java 8 Optional Tutorial](/jBlOCstVT3W0iG9KgSZ9Jg) ::: 首頁 [Kai 個人技術 Hackmd](/2G-RoB0QTrKzkftH2uLueA) ###### tags: `Spring Boot`,`w3HexSchool`