2014年7月29日

Java 8 Stream Api簡介

上篇介紹完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為輸入參數類型為T,輸出一個布林值。回傳為true,則代表此元素要留著,回傳為false則該元素會被過濾,透過Lambda的語法來實作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"。

參考

Package java.util.stream