Like Share Discussion Bookmark Smile

J.J. Huang   2020-04-08   Java   瀏覽次數:

Java 8 | Stream (上)

Java 8 API添加了一個新的抽象稱為流Stream,可以讓你以一種聲明的方式處理資料。
Stream使用一種類似用SQL語句從資料庫查詢資料的直觀方式來提供一種對Java集合運算和表達的高階抽象。
Stream API可以極大提高Java工程師的生產力,讓工程師寫出高效率、乾淨、簡潔的程式碼。
這種風格將要處理的元素集合看作一種流, 流在管道中傳輸, 並且可以在管道的節點上進行處理, 比如篩選, 排序,聚合等。
元素流在管道中經過中間操作(intermediate operation)的處理,最後由最終操作(terminal operation)得到前面處理的結果。

什麼是 Stream?

Stream(流)是一個來自資料源的元素隊列並支持聚合操作

  • 元素是特定類型的對象,形成一個隊列。Java中的Stream並不會存儲元素,而是按需計算。
  • 資料源 流的來源。可以是集合,數組,I/O channel, 產生器generator等。
  • 聚合操作 類似SQL語句一樣的操作, 比如filter, map, reduce, find, match, sorted等。

和以前的Collection操作不同, Stream操作還有兩個基礎的特徵:

  • Pipelining:中間操作都會返回流對象本身。這樣多個操作可以串聯成一個管道, 如同流式風格(fluent style)。這樣做可以對操作進行優化, 比如延遲執行(laziness)和短路(short-circuiting)。
  • 內部迭代: 以前對集合遍歷都是通過Iterator或者For-Each的方式, 顯式的在集合外部進行迭代, 這叫做外部迭代。 Stream提供了內部迭代的方式, 通過訪問者模式(Visitor)實現。

生成流

Java 8中, 集合接口有兩個方法來生成流:

  • stream() − 為集合創建串行流。
  • parallelStream() − 為集合創建並行流。
1
2
List<String> strings = Arrays.asList("abc", "", "bc", "efg", "abcd","", "jkl");
List<String> filtered = strings.stream().filter(string -> !string.isEmpty()).collect(Collectors.toList());

forEach

Stream提供了新的方法forEach來迭代流中的每個資料。以下程式碼片段使用forEach輸出了10個隨機數:

1
2
Random random = new Random();
random.ints().limit(10).forEach(System.out::println);

輸出的結果為:(每次都不一樣)

1
2
3
4
5
6
7
8
9
10
-66046892
-520689372
1948714001
-1273889566
1556557700
-625079158
1192738185
670963331
2036672007
1227072394

map

map方法用於映射每個元素到對應的結果,以下程式碼片段使用map輸出了元素對應的平方數:

1
2
3
List<Integer> numbers = Arrays.asList(3, 2, 2, 3, 7, 3, 5);
// 獲取對應的平方數
List<Integer> squaresList = numbers.stream().map( i -> i*i).distinct().collect(Collectors.toList());

squaresList為:

1
[9, 4, 49, 25]

filter

filter方法用於通過設置的條件過濾出元素。以下程式碼片段使用 filter方法過濾出空字符串:

1
2
3
List<String>strings = Arrays.asList("abc", "", "bc", "efg", "abcd","", "jkl");
// 獲取空字符串的數量
long count = strings.stream().filter(string -> string.isEmpty()).count();

count為:

1
2

limit

limit方法用於獲取指定數量的流。以下程式碼片段使用limit方法印出10條資料:

1
2
Random random = new Random();
random.ints().limit(10).forEach(System.out::println);

輸出的結果為:(每次都不一樣)

1
2
3
4
5
6
7
8
9
10
817080995
-22725850
-966417209
1911363354
-339806648
1756412403
1301090797
-1468329974
670791451
58492884

sorted

sorted方法用於對流進行排序。以下程式碼片段使用sorted方法對輸出的10個隨機數進行排序:

1
2
Random random = new Random();
random.ints().limit(10).sorted().forEach(System.out::println);

輸出的結果為:(排序)

1
2
3
4
5
6
7
8
9
10
-1374877061
-1060567933
-896527306
-722725458
-412068101
-41244032
119875982
388041556
637913506
1828715765

統計

另外,一些產生統計結果的收集器也非常有用。它們主要用於intdoublelong等基本類型上,它們可以用來產生類似如下的統計結果。

1
2
3
4
5
6
7
8
List<Integer> numbers = Arrays.asList(3, 2, 2, 3, 7, 3, 5);
 
IntSummaryStatistics stats = numbers.stream().mapToInt((x) -> x).summaryStatistics();
 
System.out.println("列表中最大的數 : " + stats.getMax());
System.out.println("列表中最小的數 : " + stats.getMin());
System.out.println("所有數之和 : " + stats.getSum());
System.out.println("平均數 : " + stats.getAverage());

輸出的結果為:

1
2
3
4
列表中最大的數 : 7
列表中最小的數 : 2
所有數之和 : 25
平均數 : 3.5714285714285716

簡單流程說明

1
2
3
+--------------------+       +------+   +------+   +---+   +-------+
| stream of elements +-----> |filter+-> |sorted+-> |map+-> |collect|
+--------------------+ +------+ +------+ +---+ +-------+

以上的流程轉換為Java程式為:

1
2
3
4
5
6
List<Integer> transactionsIds = 
widgets.stream()
.filter(b -> b.getColor() == RED)
.sorted((x,y) -> x.getWeight() - y.getWeight())
.mapToInt(Widget::getWeight)
.sum();

Stream之上的操作可分為中間操作和晚期操作

中間操作:會返回一個新的stream——執行一個中間操作(例如filter)並不會執行實際的過濾操作,而是創建一個新的stream,並將原stream中符合條件的元素放入新創建的stream

晚期操作:(例如forEach或者sum),會遍歷stream並得出結果或者附帶結果;在執行晚期操作之後,stream處理線已經處理完畢,就不能使用了。在幾乎所有情況下,晚期操作都是立刻對stream進行遍歷。

範例

Stream API極大得簡化了集合操作(​​不止是集合),首先看下這個叫Task的類:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class Streams  {
private enum Status {
OPEN, CLOSED
};

private static final class Task {
private final Status status;
private final Integer points;

Task( final Status status, final Integer points ) {
this.status = status;
this.points = points;
}

public Integer getPoints() {
return points;
}

public Status getStatus() {
return status;
}

@Override
public String toString() {
return String.format( "[%s, %d]", status, points );
}
}
}

Task類有一個分數(或偽複雜度)的概念,另外還有兩種狀態:OPEN或者CLOSED。現在假設有一個task集合:

1
2
3
4
5
final Collection< Task > tasks = Arrays.asList(
new Task( Status.OPEN, 5 ),
new Task( Status.OPEN, 13 ),
new Task( Status.CLOSED, 8 )
);

首先:在這個task集合中一共有多少個OPEN狀態的點?在Java 8之前,要解決這個問題,則需要使用foreach循環遍歷task集合;但是在Java 8中可以利用streams解決:包括一系列元素的列表,並且支持順序和並行處理。

1
2
3
4
5
6
7
8
// Calculate total points of all active tasks using sum()
final long totalPointsOfOpenTasks = tasks
.stream()
.filter( task -> task.getStatus() == Status.OPEN )
.mapToInt( Task::getPoints )
.sum();

System.out.println( "Total points: " + totalPointsOfOpenTasks );

輸出的結果為:

1
Total points: 18
  1. tasks集合被轉換成stream表示;
  2. stream上的filter操作會過濾掉所有CLOSEDtask
  3. mapToInt操作基於每個task實例的Task::getPoints方法將task流轉換成Integer集合;
  4. 通過sum方法計算總和,得出最後的結果。

總結

這邊大致上稍為介紹了Stream的使用,透過範例並且實際去執行,在將來使用的時候才會有所印象;而這邊將Stream分成上下篇,下篇將會針對parallelCollectors特別說明。


註:以上參考了
Stream 與平行化
Java 8 Lambda新語法,簡化程式,增強效能
Java 8 新特性
Java 8的新特性—终极版
现代化 Java - Java8 指南