Stream 簡介
Stream 是什麼
Classes to support functional-style operations on streams of elements, such as map-reduce transformations on collections.
Stream 是 Java 8 新特性,可對 Stream 中元素進行函數式編程操作,例如 map-reduce。
先來看一段代碼:
int sum = widgets.stream()
.filter(b -> b.getColor() == RED)
.mapToInt(b -> b.getWeight())
.sum();
這段 Java 代碼看起來是不是像通過 SQL 來操作集合:
select sum(weight) from widgets where color='RED';
Stream 類型
java.util.stream
包下提供了以下四種類型的 Stream:
Stream
: 對象類型對應的 StreamIntStream
: 基本類型 int 對應的 StreamLongStream
: 基本類型 long 對應的 StreamDoubleStream
: 基本類型 double 對應的 Stream
如何獲得 Stream
Collection to Stream
List
、Set
等 Collection
接口的實現類,可以通過 Collection.stream()
或 Collection.parallelStream()
方法返回 Stream 對象:
List<String> stringList = ...;
Stream<String> stream = stringList.stream();
Array to Stream
可以通過靜態方法 Arrays.stream(T[] array)
或 Stream.of(T... values)
將數組轉爲 Stream:
String[] stringArray = ...;
Stream<String> stringStream1 = Arrays.stream(stringArray); // 方法一
Stream<String> stringStream2 = Stream.of(stringArray); // 方法二
基本類型數組可以通過類似的方法轉爲 IntStream
、LongStream
、DoubleStream
:
int[] intArray = {1, 2, 3};
IntStream intStream1 = Arrays.stream(intArray);
IntStream intStream2 = IntStream.of(intArray);
另外, Stream.of(T... values)
、IntStream.of(int... values)
等靜態方法支持 varargs(可變長度參數),可直接創建 Stream:
IntStream intStream = IntStream.of(1, 2, 3);
Map to Stream
Map 本身不是 Collection 的實現類,沒有 stream()
或 parallelStream()
方法,可以通過 Map.entrySet()
、Map.keySet()
、Map.values()
返回一個 Collection
:
Map<Integer, String> map = ...;
Stream<Map.Entry<Integer, String>> stream = map.entrySet().stream();
其他
String
按字符拆分成 IntStream
:
String s = "Hello World";
IntStream stringStream = s.chars(); // 返回將字符串每個 char 轉爲 int 創建 Stream
BufferedReader
生成按行分隔的 Stream<String>
:
BufferedReader bufferedReader = ...;
Stream<String> lineStream = bufferedReader.lines();
IntStream
、LongStream
提供了靜態方法 range
生成對應的 Stream:
IntStream intStream = IntStream.range(1, 5); // 1,2,3,4 (不包含5)
Stream 的方法
intermediate operation 和 terminal operation
Stream operations are divided into intermediate and terminal operations, and are combined to form stream pipelines. A stream pipeline consists of a source (such as a
Collection
, an array, a generator function, or an I/O channel); followed by zero or more intermediate operations such asStream.filter
orStream.map
; and a terminal operation such asStream.forEach
orStream.reduce
.
Stream 操作分爲 中間操作(intermediate operation)和 最終操作(terminal operation),這些操作結合起來形成 stream pipeline。stream pipeline 包含一個 Stream 源,後面跟着零到多個 intermediate operations(例如 Stream.filter
、Stream.map
),再跟上一個 terminal operation(例如 Stream.forEach
、Stream.reduce
)。
intermediate operation 用於對 Stream 中元素處理和轉換,terminal operation 用於得到最終結果。
例如在本文開頭的例子中,包含以下 intermediate operation 和 terminal operation:
int sum = widgets.stream()
.filter(b -> b.getColor() == RED) // intermediate operation
.mapToInt(b -> b.getWeight()) // intermediate operation
.sum(); // terminal operation
intermediate operation
Intermediate operations return a new stream. They are always lazy; executing an intermediate operation such as filter() does not actually perform any filtering, but instead creates a new stream that, when traversed, contains the elements of the initial stream that match the given predicate. Traversal of the pipeline source does not begin until the terminal operation of the pipeline is executed.
intermediate operation 會再次返回一個新的 Stream,所以可以支持鏈式調用。
intermediate operation 還有一個重要特性,延遲(lazy)性:
IntStream.of(0, 1, 2, 3).filter(i -> {
System.out.println(i);
return i > 1;
});
以上這段代碼並不會輸出:1 2 3 4
,實際上這段代碼運行後沒有任何輸出,也就是 filter
並未執行。因爲 filter
是一個 intermediate operation,如果想要 filter
執行,必須加上一個 terminal operation:
IntStream.of(0, 1, 2, 3).filter(i -> {
System.out.println(i);
return i > 1;
}).sum();
intermediate operation 常用方法
filter
: 按條件過濾,類似於 SQL 中的where
語句limit(long n)
: 截取 Stream 的前 n 條數據,生成新的 Stream,類似於 MySQL 中的limit n
語句skip(long n)
: 跳過前 n 條數據,結合limit
使用stream.skip(offset).limit(count)
,效果相當於 MySQL 中的LIMIT offset,count
語句sorted
: 排序,類似於 SQL 中的order by
語句distinct
: 排除 Stream 中重複的元素,通過equals
方法來判斷重複,這個和 SQL 中的distinct
類似boxed
: 將IntStream
、LongStream
、DoubleStream
轉換爲Stream<Integer>
、Stream<Long>
、Stream<Double>
peek
: 類似於forEach
,二者區別是forEach
是 terminal operation,peek
是 intermediate operationmap
、mapToInt
、mapToLong
、mapToDouble
、mapToObj
: 這些方法會傳入一個函數作爲參數,將 Stream 中的每個元素通過這個函數轉換,轉換後組成一個新的 Stream。mapToXxx
中的 Xxx 表示轉換後的元素類型,也就是傳入的函數返回值,例如 mapToInt 就是將原 Stream 中的每個元素轉爲 int 類型,最終返回一個 IntStreamflatMap
、flatMapToInt
、flatMapToLong
、flatMapToDouble
: 類似map
、mapToXxx
,不同的是flatMap
會將一個元素轉爲一個 Stream,其中可包含0到多個元素,最終將每個 Stream 中的所有元素組成一個新的 Stream 返回
map、flatMap 區別
map
和 flatMap
的區別就是 map
是一對一,flatMap
是一對零到多,可以用下圖簡單說明:
-
map
示例通過
mapToInt
獲取一個字符串集合中每個字符串長度:Stream<String> stringStream = Stream.of("test1", "test23", "test4"); IntStream intStream = stringStream.mapToInt(String::length);
通過
String.length
函數可以將每個 String 轉爲一個 int,最終組成一個 IntStream。以上代碼中的stringStream
和intStream
中的元素是一一對應的,每個字符串對應一個長度,兩個 Stream 的元素數量是一致的。 -
flatMap
示例通過
flatMapToInt
將一個字符串集合中每個字符串按字符拆分,組成一個新的 Stream:Stream<String> stringStream = Stream.of("test1", "test23", "test4"); IntStream intStream = stringStream.flatMapToInt(String::chars);
每個字符串按字符拆分後可能會得到 0 到多個字符,最終得到的
intStream
元素數量和stringStream
的元素數量可能不一致。
以下表格列出了所有map相關的方法以及轉換規則:
Stream | 方法 | 函數類型 | 函數參數 | 函數返回值 | 轉換後 |
---|---|---|---|---|---|
Stream<T> |
map |
Function |
T |
R |
Stream<R> |
Stream<T> |
mapToInt |
ToIntFunction |
T |
int |
IntStream |
Stream<T> |
mapToLong |
ToLongFunction |
T |
long |
LongStream |
Stream<T> |
mapToDouble |
ToDoubleFunction |
T |
double |
DoubleStream |
Stream<T> |
flatMap |
Function |
T |
Stream<R> |
Stream<R> |
Stream<T> |
flatMapToInt |
Function |
T |
IntStream |
IntStream |
Stream<T> |
flatMapToLong |
Function |
T |
LongStream |
LongStream |
Stream<T> |
flatMapToDouble |
Function |
T |
DoubleStream |
DoubleStream |
IntStream |
map |
IntUnaryOperator |
int |
int |
IntStream |
IntStream |
mapToLong |
IntToLongFunction |
int |
long |
LongStream |
IntStream |
mapToDouble |
IntToDoubleFunction |
int |
double |
DoubleStream |
IntStream |
mapToObj |
IntFunction |
int |
R |
Stream<R> |
IntStream |
flatMap |
IntFunction |
int |
IntStream |
IntStream |
LongStream |
map |
LongUnaryOperator |
long |
long |
LongStream |
LongStream |
mapToInt |
LongToIntFunction |
long |
int |
IntStream |
LongStream |
mapToDouble |
LongToDoubleFunction |
long |
double |
DoubleStream |
LongStream |
mapToObj |
LongFunction |
long |
R |
Stream<R> |
LongStream |
flatMap |
LongFunction |
long |
LongStream |
LongStream |
DoubleStream |
map |
DoubleUnaryOperator |
double |
double |
DoubleStream |
DoubleStream |
mapToInt |
DoubleToIntFunction |
double |
int |
IntStream |
DoubleStream |
mapToLong |
DoubleToLongFunction |
double |
long |
LongStream |
DoubleStream |
mapToObj |
DoubleFunction |
double |
R |
Stream<R> |
DoubleStream |
flatMap |
DoubleFunction |
double |
DoubleStream |
DoubleStream |
例如對一個 Stream<Stirng>
執行 stream.mapToInt(String::length)
,可以理解爲將一個參數爲 String
返回值爲 int
的函數 String::length
傳入 mapToInt
方法作爲參數,最終返回一個 IntStream
。
terminal operation
Terminal operations, such as Stream.forEach or IntStream.sum, may traverse the stream to produce a result or a side-effect. After the terminal operation is performed, the stream pipeline is considered consumed, and can no longer be used; if you need to traverse the same data source again, you must return to the data source to get a new stream.
當 terminal operation 執行過後,Stream 就不能再使用了,如果想要再使用就必須重新創建一個新的 Stream:
IntStream intStream = IntStream.of(1, 2, 3);
intStream.forEach(System.out::println); // 第一次執行 terminal operation forEach 正常
intStream.forEach(System.out::println); // 第二次執行會拋出異常 IllegalStateException: stream has already been operated upon or closed
terminal operation 常用方法
forEach
: 迭代StreamtoArray
: 轉爲數組max
: 取最大值min
: 取最小值sum
: 求和count
: Stream 中元素數量average
: 求平均數findFirst
: 返回第一個元素findAny
: 返回流中的某一個元素allMatch
: 是否所有元素都滿足條件anyMatch
: 是否存在元素滿足條件noneMatch
: 是否沒有元素滿足條件reduce
: 執行聚合操作,上面的sum
、min
、max
方法一般是基於reduce
來實現的collect
: 執行相對reduce
更加複雜的聚合操作,上面的average
方法一般是基於collect
來實現的
reduce
先看一段使用 reduce
來實現 sum
求和的代碼:
IntStream intStream = IntStream.of(1, 2, 4, 5, 8);
int sum = intStream.reduce(0, Integer::sum);
或者
IntStream intStream = IntStream.of(1, 2, 4, 5, 8);
int sum = intStream.reduce(0, (a, b) -> a + b);
上面例子中的 reduce
方法有兩個參數:
identity
: 初始值,當 Stream 中沒有元素時也會作爲默認值返回accumulator
: 一個帶有兩個參數和一個返回值的函數,例如上面代碼中的Integer::sum
或者(a, b) -> a + b
求和函數
以上代碼等同於:
int result = identity;
for (int element : intArray)
result = Integer.sum(result, element); // 或者 result = result + element;
return result;
collect
先看一段代碼,將一個 Stream<String>
中的元素拼接成一個字符串,如果用 reduce
可以這樣實現:
Stream<String> stream = Stream.of("Hello", "World");
String result = stream.reduce("", String::concat); // 或者 String result = stream.reduce("", (a, b) -> a + b);
當 Stream 中有大量元素時,用字符串拼接方式性能會大打折扣,應該使用性能更高的 StringBuilder
,可以通過 collect
方法來實現:
Stream<String> stream = Stream.of("Hello", "World");
StringBuilder result = stream.collect(StringBuilder::new, StringBuilder::append, StringBuilder::append);
上面例子中的 collect
方法有三個參數:
supplier
: 傳入一個函數,用於創建一個存放聚合計算結果的容器(result container),例如上面的例子中第一個傳入參數StringBuilder::new
,該函數用於創建一個新的StringBuilder
來存放拼接字符串的結果accumulator
: 傳入一個函數,用於將 Stream 中的一個元素合併到 result container 中,例如上面的例子中第二個傳入參數StringBuilder::append
,該函數用於將 Stream 中的字符串 append 到 StringBuilder 中combiner
: 傳入一個函數,用於將兩個 result container 合併,這個函數一般會在並行流中用到,例如上面的例子中第三個傳入參數StringBuilder::append
,該函數用於將兩個StringBuilder
合併
下面再用 collect
實現求平均數:
計算平均數需要有兩個關鍵的數據:數量、總和,首先需要創建一個 result container 存放這兩個值,並定義相關方法:
public class Averager {
private int total = 0;
private int count = 0;
public double average() {
return count > 0 ? ((double) total) / count : 0;
}
public void accumulate(int i) {
total += i;
count++;
}
public void combine(Averager other) {
total += other.total;
count += other.count;
}
}
通過計算平均值:
IntStream intStream = IntStream.of(1, 2, 3, 4);
Averager averager = intStream.collect(Averager::new, Averager::accumulate, Averager::combine);
System.out.println(averager.average()); // 2.5
Collector
Stream
接口中還有一個 collect
的重載方法,僅有一個參數:collect(Collector collector)
。
Collector
是什麼:
This class encapsulates the functions used as arguments in the collect operation that requires three arguments (supplier, accumulator, and combiner functions).
Collector
實際上就是一個包含 supplier
、accumulator
、combiner
函數的類,可以實現對常用聚合算法的抽象和複用。
例如將 Stream<String>
中的元素拼接成一個字符串,用 Collector
實現:
public class JoinCollector implements Collector<String, StringBuilder, String> {
@Override
public Supplier<StringBuilder> supplier() {
return StringBuilder::new;
}
@Override
public BiConsumer<StringBuilder, String> accumulator() {
return StringBuilder::append;
}
@Override
public BinaryOperator<StringBuilder> combiner() {
return StringBuilder::append;
}
@Override
public Function<StringBuilder, String> finisher() {
return StringBuilder::toString;
}
@Override
public Set<Characteristics> characteristics() {
return Collections.emptySet();
}
}
或者直接用 Collector.of()
靜態方法直接創建一個 Collector
對象:
Collector<String, StringBuilder, String> joinCollector = Collector.of(StringBuilder::new,
StringBuilder::append,
StringBuilder::append,
StringBuilder::toString);
Stream<String> stream = Stream.of("Hello", "World");
String result = stream.collect(joinCollector);
另外還有一個更簡單的方式,使用 Collectors.joining()
:
Stream<String> stream = Stream.of("Hello", "World");
String result = stream.collect(Collectors.joining());
Collectors
在 java.util.stream.Collectors
: 中提供了大量常用的 Collector
:
Collectors.toList()
: 將 Stream 轉爲 ListCollectors.toSet()
: 將 Stream 轉爲 SetCollectors.joining()
: 將 Stream 中的字符串拼接Collectors.groupingBy()
: 將 Stream 中的元素分組,類似於 SQL 中的group by
語句Collectors.counting()
: 用於計算 Stream 中元素數量,stream.collect(Collectors.counting())
等同於stream.count()
Collectors.averagingDouble()
、Collectors.averagingInt()
、Collectors.averagingLong()
: 計算平均數
上面只列出了 Collectors
中的一部分方法,還有其他常用的方法可以參考文檔。
下面列出一些 Collectors
的實用示例:
- 將 Stream 轉爲 List:
Stream<String> stream = Stream.of("Hello", "World"); List<String> list = stream.collect(Collectors.toList());
- 將學生(Student)按年齡分組,返回每個年齡對應的學生列表:
Stream<Student> stream = ...; Map<Integer, List<Student>> data = stream.collect(Collectors.groupingBy(Student::getAge));
- 將學生(Student)按年齡分組,返回每個年齡對應的學生數量,實現和 SQL 一樣的結果:
select age,count(*) from student group by age
:Stream<Student> stream = ...; Map<Integer, Long> data = stream.collect(Collectors.groupingBy(Student::getAge, Collectors.counting()));
- 計算學生(Student)年齡平均數:
Stream<Student> stream = ...; Double data = stream.collect(Collectors.averagingInt(Student::getAge)); // 或者可以 double average = stream.mapToInt(Student::getAge).average().getAsDouble();
參考文檔
- https://docs.oracle.com/javase/8/docs/api/java/util/stream/package-summary.html
- https://docs.oracle.com/javase/tutorial/collections/streams/reduction.html
- https://docs.oracle.com/javase/tutorial/collections/streams/examples/ReductionExamples.java