Java 8 新增了一個新的 Stream package 專門用來處理集合(collection),搭配 lambda expression,在處理集合方面變得更加方便。
當 stream 的方法回傳的值是 Stream 型別時,並不會實際去執行運算,而是當該方法回傳除了 Stream 型別以外的值時(包含void),才真正執行運算。所以要記住 stream 方法的串接規則,中間串接的永遠是回傳 Stream 型別的方法,只有最後一個才是串接傳回某個非 Stream 型別的值的方法。在 API 文件中,屬於終止操作的方法,會有「 This is a terminal operation. 」這樣的標示,表示這個方法只能串接在最後一個。
Stream 提供 filter、sort、map 等功能。Stream 和 Collection 的區別是,Collection 是一種靜態的資料結構,使用的是記憶體空間,而 Stream 則是運算,使用的是 CPU。
範例:印出1~10的數字
使用 for 迴圈:for (int i = 0; i < 10; i++) {
System.out.println(i);
}
使用 IntStream:
IntStream.range(0, 10).forEach(i -> System.out.println(i));
//Use Method Reference
IntStream.range(0, 10).forEach(System.out::println);
原有的寫法要用到 3 行,雖然你可以故意把它寫成 1 行,可是在閱讀上其實不容易。使用 Stream 來執行,不僅簡短,對於這段程式碼的意圖也很清楚。Java 8 新加入的方法參考(Method References),是 lambda 表達式的一種,當你的 lambda 表達式呼叫一個既有的方法但又什麼事都沒做時,就可以使用 Method References,使用方式是
類別名稱::方法名稱
當我們查 IntStream API 的 range()方法,你可以看到它是回傳 IntStream:
static IntStream range(int startInclusive, int endExclusive)
所以 range 並不會直正執行迴圈計算,接下來看看 forEach() 方法:
void forEach(IntConsumer action)
它的回傳值是 void,也就是不回傳值,因為並非 Stream 型別,所以這個方法會真正去執行迴圈計算。在使用 Stream 方法時,記得都要這樣使用。
collect
collect 方法也很常用,可以回傳集合,例如:List<String> names = Stream.of("Tony", "Tom", "Jonn").collect(Collectors.toList());
List<String> names2 = Arrays.asList("Tony", "Tom", "Jonn");
System.out.println(names.toString());
System.out.println(names2.toString());
//[Tony, Tom, Jonn]
//[Tony, Tom, Jonn]
使用 Stream 的 of 方法指定集合的資料,然後用 collect 方法來收集,必須指定收集的型式,這裡指定為 toList。這段程式碼產生的結果和 Arrays.asList 的結果相同。你可以以相同的方式取得 Set 集合:
Set<String> names = Stream.of("Tony", "Tony", "Tony", "Tom", "Jonn").collect(Collectors.toSet());
System.out.println(names.toString());
//[Tony, Tom, Jonn]
註:Set 集合會去除重覆。
Collectors.joining
在 collect() 方法中可以傳入 Collector 介面的參數,而 Collectors 類別已經實作了許多方法可以用,這裡介紹一個好用的 joining() 方法。有時候我們會需要使用 StringBuilder 搭配 for 迴圈來組合一些資料成一段字串,寫法如下:
private void stringJoinUseStringBuilder() {
StringBuilder sb = new StringBuilder("[");
int i = 0;
for (Product p : productList) {
String name = p.name;
sb.append(name);
if (++i < productList.size()){
sb.append(",");
}
}
sb.append("]");
System.out.println(sb.toString());
//[Apple,Banana,Cheery,Orange,WaterMelone]
}
如果我沒有貼上輸出結果,你可能要讀一小段時間才能知道這段程式碼在幹嘛,如果把它改為使用 stream 來寫的話,會是這樣:
private void stringJoinUseStream() {
String result = productList.stream()
.map(p -> p.name)
.collect(Collectors.joining(",", "[", "]"));
System.out.println(result);
//[Apple,Banana,Cheery,Orange,WaterMelone]
}
joining 的參數分別是定位符號、前置字元、後置字元,這樣讀起來是不是好讀多了。Collectors 還有很多好用的方法,可以自行研究一下。
map
使用時機
資料的轉換。當你有一個方法(method)它的用途會將某個輸入資料轉換成另一個資料輸出時,map 可以讓你使用這個方法。輸入輸出:T -> R
範例:使用 foreach 及 Stream.map 的比較
//foreach
List<String> names = new ArrayList<>();
for (String name : asList("tony", "tom", "john")) {
String upperName = name.toUpperCase();
names.add(upperName);
}
System.out.println(names.toString());
//[TONY, TOM, JOHN]
//Stream
List<String> names2 = Stream.of("tony", "tom", "john")
.map(name -> name.toUpperCase())
.collect(toList());
System.out.println(names2.toString());
//[TONY, TOM, JOHN]
map 方法中的 lambda 表達式 name -> name.toUpperCase() 也可以換成 Method Reference 的方式:String::toUpperCase,意圖會更明確,而且 name 其實是多餘的。
Type Inferred (型別推斷)
在上面的範例中,你會看到 foreach 的例子裡,new ArrayList<>(); 並沒有寫成 new ArrayList<String>();,這也是 Java 8 的新功能:型別推斷。因為我們宣告的 names 指定為 List<String> 型別,編譯器即可推斷出 ArrayList 也是 <String>,因此可以省略不寫。只要可以明確得知物件的型別資訊,都可以省略不寫。
filter
使用時機
資料的檢查。當你要用迴圈來檢查集合中的每個元素是否符合你的期望時。輸入輸出:T -> boolean
範例:使用 foreach 和 Stream.filter 的比較
//foreach
List<String> names = new ArrayList<>();
for (String name : asList("Tony", "Tom", "John", "Andy")) {
if (name.startsWith("T")) {
names.add(name);
}
}
System.out.println(names.toString());
//[Tony, Tom]
//stream
List<String> names2 = Stream.of("Tony", "Tom", "John", "Andy")
.filter(name -> name.startsWith("T"))
.collect(Collectors.toList());
System.out.println(names2.toString());
//[Tony, Tom]
兩個例子一比較,應該就能清楚知道 filter 的用途了。
flatMap
使用時機
平面化集合資料。將不同集合中的資料串接在一起,成為在一起的一組集合。範例:使用 Stream.flatMap
List<String> allNames = Stream.of(asList("Tony", "Tom", "John"),
asList("Amy", "Emma", "Iris"))
.flatMap(names -> names.stream())
.collect(Collectors.toList());
System.out.println(allNames.toString());
//[Tony, Tom, John, Amy, Emma, Iris]
count
使用時機
計算集合的元素數量。範例:使用 Stream.count
long count = Stream.of("Tony", "Tom", "John", "Amy", "Emma", "Iris").count();
System.out.println("count: " + count);
//count: 6
max, min
使用時機
取得集合中的最大值或最小值。範例:使用 Stream.max 及 Stream.min
int max = Stream.of(120,24,59,63,11,74)
.max(Comparator.comparing(n -> n))
.get();
System.out.println("max: " + max);
//max: 120
int min = Stream.of(120,24,59,63,11,74)
.min(Comparator.comparing(n -> n))
.get();
System.out.println("min: " + min);
//min: 11
這裡要注意一下,max 和 min 方法的回傳值是 Optional<t>,這是 Java 8 的新功能。它不是一般的值,它表示的是這個方法回傳的值,可能有值,也可能無值,也就是兩種可能性,因此我們必須使用 Optional 的 get 方法來取得真正的值。max 及 min 是屬於終止操作。
sorted
使用時機
要對集合內容進行排序時。範例:使用 Stream.sorted
//ASC
List<Integer> sortedAsc = Stream.of(120,24,59,63,11,74)
.sorted()
.collect(toList());
System.out.println("sorted asc: " + sortedAsc);
//sorted asc: [11, 24, 59, 63, 74, 120]
//DESC
List<Integer> sortedDesc = Stream.of(120,24,59,63,11,74)
.sorted((n1,n2) -> n2.compareTo(n1))
.collect(toList());
System.out.println("sorted desc: " + sortedDesc);
//sorted desc: [120, 74, 63, 59, 24, 11]
循序運算(sequential)及並行運算(parallel)
Stream 提供兩種執行運算的方式,一種是循序運算,只會在單一個執行緒上運算,另一種並行運算則是在多個執行緒上同時運算,並行運算的方式可以充份利用 CPU 多核多執行緒的特點,因此執行速度會比較快。範例:循序運算及並行運算的執行速度比較
主程式:List<Integer> randomNumbers = getRandomNumbers();
sequential(randomNumbers);
parallel(randomNumbers);
先產生一個極大數量的亂數集合,之後透過這兩種運算方式來進行排序的動作,看看執行速度有什麼差別。方法實作:
private List<Integer> getRandomNumbers() {
List<Integer> randomNumbers = new ArrayList<>();
IntStream.range(0, 1000000)
.forEach(n -> {
int rnd = (int)(Math.random() * 10000);
randomNumbers.add(rnd);
});
return randomNumbers;
}
//循序運算
private void sequential(List<Integer> randomNumbers) {
long start = System.nanoTime();
List<Integer> sorted = randomNumbers.stream().sequential().sorted().collect(toList());
long end = System.nanoTime();
long duration = TimeUnit.NANOSECONDS.toMillis(end - start);
System.out.println("sequntial duration: " + duration + "(ms)");
//sequntial duration: 757(ms)
}
//並行運算
private void parallel(List<Integer> randomNumbers) {
long start = System.nanoTime();
List<Integer> sorted = randomNumbers.stream().parallel().sorted().collect(toList());
long end = System.nanoTime();
long duration = TimeUnit.NANOSECONDS.toMillis(end - start);
System.out.println("parallel duration: " + duration + "(ms)");
//parallel duration: 535(ms)
}
因為這個結果取決於 CPU 的運算,所以每次執行的結果都不會相同,不同電腦上執行的結果也會不同,但結果大致都是並行運算快過循序運算。
本文網址:https://blog.tonycube.com/2015/10/java-java8-3-stream.html
由 Tony Blog 撰寫,請勿全文複製,轉載時請註明出處及連結,謝謝 😀
由 Tony Blog 撰寫,請勿全文複製,轉載時請註明出處及連結,謝謝 😀
想問關於filter這個功能
回覆刪除如果當我想過濾的條件是多個的時候應該要怎麼寫
我自己室友嘗試過這個
http://stackoverflow.com/questions/36246998/stream-filter-of-1-list-based-on-another-list
但是不成功 結果並不如預期
在 filter 中,你實際上是在寫 if 條件式,所以你可以這樣寫:
刪除假設有個 Person 的類別,內有 age,gender 等屬性,
有個 List 中有多個 Person,然後
...
.filter(p -> p.age >= 18 && p.gender.equals("female"))
或是
...
.filter(p -> p.age >= 18)
.filter(p -> p.gender.equals("female"))
你可以串接多個過濾條件,這樣其實看得比較清楚。
感謝妳的回覆 似乎有點頭緒了
刪除難怪每次都是指過濾了最後那個條件
因為我的迴圈放置的地方有問題要調整
Lamdba 效能未提很多(非parallel)
回覆刪除易讀性跟維護性也不好,沒有什麼非要用的必要性。
very readable
刪除請教 Stream 和 IntStream 分別是甚麼?
回覆刪除一樣的東西,只是 IntStream 只能用在整數原始型別 int ,有針對 int 增加額外的功能可用,例如 IntStream 有 range() 可用,Stream 就沒有。
刪除以 API 文件來看它們的區別,請點選 Method Summary 中的「Default Methods」:
* https://docs.oracle.com/javase/8/docs/api/java/util/stream/Stream.html
* https://docs.oracle.com/javase/8/docs/api/java/util/stream/IntStream.html
差在兩個串流處理的資料類型不一樣
刪除Stream處理的是 Integer類 (裝箱後的int)
IntStream處理的是int (基本資料類型int)
會有boxing跟unboxing的overhead問題~~
可以將IntStream想像成,在使用Stream在處理整數的一種分支(提供處理int的串流)。