大家好,我是 "為了拿到金角獎盃而努力著" 的文毅青年 - Kai
世界上本沒有路,有了腿便有了路
承上周介紹了 Lambda 後,今天繼續介紹另外一個歷史級的 feature – Stream
Stream 是 JDK 8 版本提供用來處理 Collections 物件的一套特殊處理機制,同時也是將 Java 原先的迭代處理方式由 外部迭代,改為 內部迭代 的重要推手。
搭配上 Lambda 表示式,讓開發人員在寫 code 上的發揮如虎添翼,省去了許多繁文縟節,可以更專心地在刻劃業務邏輯上,重複性質的過程盡可能的避免,大大的提升了時程上的效益,也讓 Java 在開發上終於不再被太多的 前處理 綁住,逐漸看齊新興高階語言的流暢。
先補上一個簡單的架構圖,以便清楚明瞭何謂外部迭代、內部迭代。
在介紹 Stream 前,首先要了解的是關於 Java 的迭代,也就是平時工程師在處理 Collection 物件時候會需要用到的 迴圈。
小知識 遞迴與迭代?
迭代演算法指的是那些經由重複性運算處理,得以讓變數的原值每經過一次運算後能夠得到新值的方法。
遞迴
是設計與描述演算法的工具,常在使用演算法前被拿出做討論,而能夠採用遞迴的演算法通常具備幾種特質:
- 集合能夠被拆解為許多小集合
- 小集合的解匯集後可以成為大集合的解
- 當一個集合無法被拆分成更多的小集合時則視為個體物件,將執行運算流程後返回解,並提供給上一層的集合,以讓其進行運算
- 每一個集合體(無論大、小、個體物件),都能夠適用於同一種分解和運算流程。
Java 中常見的 外部迭代 莫過於 For Loop、For Each、While、Do While,而在使用這些 function 的時候,開發人員還是得自己刻畫其邏輯架構、判斷依據、輪迴方式等,其實對於開發人員來說,最重要的莫過於下列的幾點:
也許有人會說,其實我已經把所有的東西都說出來了不是嗎? 且聽後續娓娓道來,慢慢讓你體會 Stream 的便利之處。
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 的操作都屬於惰性,這表示在進行這些操作時,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);
}
}
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)
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 的優勢,我們繼續往下做一些分類和處理
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)])
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)])
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
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
傳統的外部迭代
你會發現用傳統的外部迭代方式會很難去對 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
Spring Boot
,w3HexSchool