owned this note
owned this note
Published
Linked with GitHub
# 六角鼠年鐵人賽 Week 17 - Spring Boot - 番外篇 Java 8 Stream Tutorial
==大家好,我是 "為了拿到金角獎盃而努力著" 的文毅青年 - Kai==
## 劇中佳句 讓子彈飛
:::info
世界上本沒有路,有了腿便有了路
:::
## 主題
承上周介紹了 **Lambda** 後,今天繼續介紹另外一個歷史級的 feature -- **Stream**
**Stream** 是 JDK 8 版本提供用來處理 Collections 物件的一套特殊處理機制,同時也是將 Java 原先的迭代處理方式由 **外部迭代**,改為 **內部迭代** 的重要推手。
搭配上 Lambda 表示式,讓開發人員在寫 code 上的發揮如虎添翼,省去了許多繁文縟節,可以更專心地在刻劃業務邏輯上,重複性質的過程盡可能的避免,大大的提升了時程上的效益,也讓 Java 在開發上終於不再被太多的 **前處理** 綁住,逐漸看齊新興高階語言的流暢。
## 迭代
先補上一個簡單的架構圖,以便清楚明瞭何謂外部迭代、內部迭代。

在介紹 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`