上篇介紹完Lambda之後,接著要來介紹 JAVA 8新增的 Stream API。
Stream介紹
Stream並不是一個儲存容器,也就是說,他並不是像List、Set、Map那種存放實際資料的容器,他是將資料來源從來源取得stream之後,透過管線(pipleline)操作,來取得你所需要的結果。
舉例來說:
今天我想知道公司的vip會員到底為公司貢獻了多少的營業額。首先,我先拿到了一百萬筆會員的資料儲存容器,我取得其容器的stream,先做初步的資料filter,只留會員資格為vip資料,之後將filter完的結果透過管線交給接下來的操作。
下一個操作執行map的動作,將資料內真正關心的資料指出來,在這邊我就可以指定,我只關心他們每個人累積消費金額為多少,map完之後在交給下一個操作處理,
最後一個操作執行reduce,將所需要的結果整理好,也就是說我可以透過reduce將所有vip的總消費金額加總起來,得知vip為公司貢獻了多少營業額。
Stream管線操作架構
有了基本的Stream能做什麼的概念之後,接著來看看Stream管線操作的架構。
整個管線操作有分成兩個部分,intermediate與terminal,其中intermediate可以有0~n個,比如說filter和map,他們就是屬於intermediate,這類的操作並不會立即生效,只會回傳新的stream回來,他們屬於lazy operation。
而terminal只能有一個,當遇到terminal operation時,則會立即生效,他們是屬於eager operation。當stream執行完terminal operation之後,則此管線已被consumed了,已經不能在被使用了,如果需要在做任何的管線操作,則必須從資料來源在產生一個新的stream,建立先的管線,才能執行管線操作。
以下為管線操作架構的整理:
source => Collection, array, generator function, I/O channel
intermediate=>Stream.filter, Stream.map
terminal operation=>Stream.forEach, Stream reduce.
Stream Api Sample
接下來舉一些例子,來說明怎麼使用Stream Api。
原始資料,List內放0~5的整數資料
List<Integer> listSource = new ArrayList<Integer>();
Collections.addAll(listSource, 0, 1, 2, 3, 4, 5);
挑出大於2的元素
可以透過filter的操作來實現,filter的官方文件寫他需要傳入Predicate介面。而在上一篇提到,
Predicate
List<Integer> listGreaterThen3 = listSource.stream()
.filter((t) -> (t > 2) ? true : false)
.collect(Collectors.toList());
// listSource = [0, 1, 2, 3, 4, 5]
// listGreaterThen2 = [3, 4, 5]
挑出第一個大於2的元素
一樣夠過filter來實現,差別在於上面的例子把filter之後得到的stream透過collect method,將結果收集到另一個List內,這邊的例子是使用findFirst method,把在此stream裡第一個大於3的元素取回。這邊需要注意的是,如果filter的條件,無法過濾出任何元素,則在使用get method時,會丟出java.util.NoSuchElementException。
Integer firstGreaterThen2 = listSource.stream().filter((t) -> (t > 2) ? true : false)
.findFirst().get();
// listSource = [0, 1, 2, 3, 4, 5]
// firstGreaterThen2 = 3
取所有大於2元素的,將每個被挑出來的元素乘以2之後,加總
先透過filter取得大於2的元素,之後透過mapToInt method,將這些元素皆乘以2,之後透過reduce方法來取得總和。下面總共有三種reduce的方式,第一種是利用IntStream.sum()來加總,第二種則是透過reduce method,自己寫加總演算法,第三種則是透過Method References,使用Interger的sum method來達成。
int sum1 = listSource.stream().filter((t) -> (t > 2) ? true : false)
.mapToInt((t) -> t * 2).sum();
int sum2 = listSource.stream().filter((t) -> (t > 2) ? true : false)
.mapToInt((t) -> t * 2).reduce(0, (i, t) -> i + t);
int sum3 = listSource.stream().filter((t) -> (t > 2) ? true : false)
.mapToInt((t) -> t * 2).reduce(0, Integer::sum);
// listSource = [0, 1, 2, 3, 4, 5]
// after filter and map = [6, 8 ,10]
// sum = 24
取所有大於2元素的,將每個被挑出來的元素乘以2之後,取最大值
先透過filter取得大於2的元素,之後透過mapToInt method,將這些元素皆乘以2,之後透過reduce方法來取得stream裡面最大值為何。下面總共有三種reduce的方式,第一種是利用IntStream.max()來求最大值,第二種則是透過reduce method,自己寫求最大值算法,第三種則是透過Method References,使用Interger的max method來達成。這邊需要注意的是,如果找不出最大的元素,則會丟出java.util.NoSuchElementException。
int max1 = listSource.stream().filter((t) -> (t > 2) ? true : false)
.mapToInt((t) -> t * 2).max().getAsInt();
int max2 = listSource.stream().filter((t) -> (t > 2) ? true : false)
.mapToInt((t) -> t * 2).reduce(0, (a, b) -> (a > b) ? a : b);
int max3 = listSource.stream().filter((t) -> (t > 2) ? true : false)
.mapToInt((t) -> t * 2).reduce(0, Integer::max);
// listSource = [0, 1, 2, 3, 4, 5]
// after filter and map = [6, 8 ,10]
// max = 10
Stream資料來源被修改
最後來看一個Stream的一個例子:
List<String> list = new ArrayList<String>();
Collections.addAll(list, "one", "two");
Stream<String> stream = list.stream();
list.add("three");
List<String> listColl = stream.collect(Collectors.toList());
// [one, two, three]
這例子內,原本有個ArrayList,裡面放著"one"、"two"兩個字串。首先我們先得到了一個此ArrayList的stream並存放在變數內,之後我們先把新的字串"three"直接加進ArrayList內,在從先前存放的stream變數收集元素放到新的List內,結果最後收集到的結果為"one"、"two"、"three"。
還記得上面的部分有說明,Stream有分成intermediate operation與terminal operation,而其中前者為lazy,後者為eager,上面例子裡,collect()屬於terminal operation,也就是說,由於ArrayList的修改是在collect()之前執行的,而stream是在遇到terminal operation才會真正去執行程式,因此上面程式裡在執行collect()時,原本的ArrayList變成了"one"、"two"、"three",因此最後stream收集到的結果,也是"one"、"two"、"three"。