Java 8 Stream 總結

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 : 對象類型對應的 Stream
  • IntStream : 基本類型 int 對應的 Stream
  • LongStream : 基本類型 long 對應的 Stream
  • DoubleStream : 基本類型 double 對應的 Stream

如何獲得 Stream

Collection to Stream

ListSetCollection 接口的實現類,可以通過 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); // 方法二

基本類型數組可以通過類似的方法轉爲 IntStreamLongStreamDoubleStream

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();

IntStreamLongStream 提供了靜態方法 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 as Stream.filter or Stream.map; and a terminal operation such as Stream.forEach or Stream.reduce.

Stream 操作分爲 中間操作(intermediate operation)和 最終操作(terminal operation),這些操作結合起來形成 stream pipeline。stream pipeline 包含一個 Stream 源,後面跟着零到多個 intermediate operations(例如 Stream.filterStream.map),再跟上一個 terminal operation(例如 Stream.forEachStream.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 : 將 IntStreamLongStreamDoubleStream 轉換爲 Stream<Integer>Stream<Long>Stream<Double>
  • peek : 類似於 forEach,二者區別是 forEach 是 terminal operation,peek 是 intermediate operation
  • mapmapToIntmapToLongmapToDoublemapToObj : 這些方法會傳入一個函數作爲參數,將 Stream 中的每個元素通過這個函數轉換,轉換後組成一個新的 Stream。mapToXxx 中的 Xxx 表示轉換後的元素類型,也就是傳入的函數返回值,例如 mapToInt 就是將原 Stream 中的每個元素轉爲 int 類型,最終返回一個 IntStream
  • flatMapflatMapToIntflatMapToLongflatMapToDouble : 類似 mapmapToXxx,不同的是 flatMap 會將一個元素轉爲一個 Stream,其中可包含0到多個元素,最終將每個 Stream 中的所有元素組成一個新的 Stream 返回

map、flatMap 區別

mapflatMap 的區別就是 map 是一對一,flatMap 是一對零到多,可以用下圖簡單說明:

在這裏插入圖片描述

  1. map 示例

    通過 mapToInt 獲取一個字符串集合中每個字符串長度:

    Stream<String> stringStream = Stream.of("test1", "test23", "test4");
    IntStream intStream = stringStream.mapToInt(String::length);
    

    通過 String.length 函數可以將每個 String 轉爲一個 int,最終組成一個 IntStream。以上代碼中的 stringStreamintStream 中的元素是一一對應的,每個字符串對應一個長度,兩個 Stream 的元素數量是一致的。

  2. 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 : 迭代Stream
  • toArray : 轉爲數組
  • max : 取最大值
  • min : 取最小值
  • sum : 求和
  • count : Stream 中元素數量
  • average : 求平均數
  • findFirst : 返回第一個元素
  • findAny : 返回流中的某一個元素
  • allMatch : 是否所有元素都滿足條件
  • anyMatch : 是否存在元素滿足條件
  • noneMatch : 是否沒有元素滿足條件
  • reduce : 執行聚合操作,上面的 summinmax 方法一般是基於 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 實際上就是一個包含 supplieraccumulatorcombiner 函數的類,可以實現對常用聚合算法的抽象和複用。

例如將 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 轉爲 List
  • Collectors.toSet() : 將 Stream 轉爲 Set
  • Collectors.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();
    

參考文檔

關注我

掃碼關注

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章