六角鼠年鐵人賽 Week 17 - Spring Boot - 番外篇 Java 8 Stream Tutorial

大家好,我是 "為了拿到金角獎盃而努力著" 的文毅青年 - Kai

劇中佳句 讓子彈飛

世界上本沒有路,有了腿便有了路

主題

承上周介紹了 Lambda 後,今天繼續介紹另外一個歷史級的 feature Stream

Stream 是 JDK 8 版本提供用來處理 Collections 物件的一套特殊處理機制,同時也是將 Java 原先的迭代處理方式由 外部迭代,改為 內部迭代 的重要推手。

搭配上 Lambda 表示式,讓開發人員在寫 code 上的發揮如虎添翼,省去了許多繁文縟節,可以更專心地在刻劃業務邏輯上,重複性質的過程盡可能的避免,大大的提升了時程上的效益,也讓 Java 在開發上終於不再被太多的 前處理 綁住,逐漸看齊新興高階語言的流暢。

迭代

先補上一個簡單的架構圖,以便清楚明瞭何謂外部迭代、內部迭代。

在介紹 Stream 前,首先要了解的是關於 Java 的迭代,也就是平時工程師在處理 Collection 物件時候會需要用到的 迴圈

小知識 遞迴與迭代?
迭代演算法

指的是那些經由重複性運算處理,得以讓變數的原值每經過一次運算後能夠得到新值的方法。

遞迴

是設計與描述演算法的工具,常在使用演算法前被拿出做討論,而能夠採用遞迴的演算法通常具備幾種特質:

  1. 集合能夠被拆解為許多小集合
  2. 小集合的解匯集後可以成為大集合的解
  3. 當一個集合無法被拆分成更多的小集合時則視為個體物件,將執行運算流程後返回解,並提供給上一層的集合,以讓其進行運算
  4. 每一個集合體(無論大、小、個體物件),都能夠適用於同一種分解和運算流程。

Java 中常見的 外部迭代 莫過於 For LoopFor EachWhileDo While,而在使用這些 function 的時候,開發人員還是得自己刻畫其邏輯架構、判斷依據、輪迴方式等,其實對於開發人員來說,最重要的莫過於下列的幾點:

  • 傳入的物件
  • 限制條件
  • 業務邏輯處理
  • 輸出結果

也許有人會說,其實我已經把所有的東西都說出來了不是嗎? 且聽後續娓娓道來,慢慢讓你體會 Stream 的便利之處。

Stream 是一種 Collection?

Stream 是 Collections Interface 底下的新規格,也就是說所有繼承、實作 Collections 的類別皆有支援。

在上篇 Lambda 表示式的文章內有提到,Java 8 後的版本可以在 Interface 增加 default 的方式,這讓 Stream 可以無痛相容所有 Java 7 的 Collections 類別。

Collections Interface Content:

default Stream<E> stream( ) { return StreamSupport.stream(spliterator(), false); }

在應用上,許多我們熟知的 Collections 類別都可以轉成 Stream,舉例:

// List 轉 Stream List.stream() // Set 轉 Stream Set.stream(); // Map 轉 Stream,需要先轉成 Set Map.entrySet().stream(); // Object [] 轉 Stream Arrays.stream(Object[]);

當然反面思考,我們也可以將 Stream 轉為特定的集合物件:

// 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

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

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 省時省力好幫手 這一篇文章

接著這一單元的主程式 StreamTest.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

  • 傳統的外部迭代
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 版的內部迭代
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 的歌手物件

  • 傳統的外部迭代
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 版的內部迭代
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 秒以上並列成集合物件

  • 傳統的外部迭代
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 版的內部迭代
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 的三種排序方式

  • Stream 版的內部迭代

// 以原先的流程來說,只需要多加一行 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 不好理解迭代中的處理狀況
省略許多繁雜的重複語法
迭代功能齊全

結語

還有許多 Stream 特別的功能 Kai 並沒有全部提到,有興趣的朋友們可以透過 Kai 簡單的介紹後,在實作中一一去接觸~
目前來說應該只有這兩篇番外是關於 Java 8 變動較大的部分,另外 Kai 還在思考是否寫一篇關於 Optional API 的部分,但相比較前兩篇來說,這個部分算是比較小的變動

Anyway,下篇文章便會揭曉XD
六角鼠年鐵人賽 Week 18 - Spring Boot - 番外篇 Java 8 Optional Tutorial

首頁 Kai 個人技術 Hackmd

tags: Spring Boot,w3HexSchool
Select a repo