JDK8 Stream 流式操作分析

原創文章, 轉載請私信. 訂閱號 tastejava 學習加思考, 仔細品味java之美

Stream 是什麼, 能幹什麼

要熟練使用 Stream 首先要知道什麼是 Stream , 下方爲源碼註釋原文

A sequence of elements supporting sequential and parallel aggregate operations.
一個包含元素的序列, 支持序列化或者並行的聚集操作

即 Stream 是元素的容器, 有些像 Collection, 但是 Stream 的專注點在於怎麼操作處理元素, 而不是存儲, 在 Stream 操作元素後最終會聚集成 Collection 或最終要的結果. Stream 只是爲了操作元素而生的一個臨時的容器.

Stream 實例的創建

要想使用 Stream 首先要獲得其實例, 總共有五類方法可以獲得Stream實例

  1. Stream.of(T t)
  2. Stream.of(T… values)
  3. Stream.generate(Supplier<T> s)
  4. Stream.iterate(final T seed, final UnaryOperator<T> f)
  5. Collection 所有子類的 parallelStream() 或 stream() 方法

上方五種方式獲取 Stream 實例代碼以及解釋如下(需要了解JDK提供的函數式接口):

log.info("第一部分 Stream 實例的獲取方式");
// Stream.of 單參數方法生成實例
Stream.of("Wonderful stream !")
		.forEach(System.out::println);
// Stream.of 可變參數方法生成實例
Stream.of("Wonderful ", "Stream ", "!")
		.forEach(System.out::print);
// 方法引用獲得 println 方法, 爲了打印一個換行符 : )
Consumer<String> consumer = System.out::println;
consumer.accept("");
// generate 方法生成無限長度流
Stream.generate(Math::random)
		.limit(2)
		.forEach(System.out::println);
// Collection default 方法生成流, Collection 的所有子類都可以獲得 Stream
List<Double> collect = Stream.generate(Math::random)
		.limit(2)
		.collect(Collectors.toList());
collect.parallelStream()
		.forEach(System.out::println);
// iterate 方法生成無限長度流, 第一個參數作爲 seed, 
// 第二個參數是個 UnaryOperator 一元操作符
Stream.iterate(1, seed -> seed + 1)
		.limit(2)
		.forEach(System.out::println);

需要注意的是, 上面獲取 Stream 實例的方法中, 如果 Stream 實例的數據源自容器 Collection 的子類時, 相應的獲取流實例的方法會將容器中的元素取出來構造實例, 而不是把整個 Collection 放進流.

Stream 的使用

Stream實例的操作

我們已經知道怎麼獲取 Stream 實例, 那麼接下來的問題就是怎麼使用 Stream 實例, 有人就是怎麼操作 Stream 實例中的元素.
Stream 提供很多方法操作實例的元素, 這些操作都支持鏈式操作, 即進行一個流操作後返回的結果也是 Stream 實例, 並且把舊的實例關閉廢棄掉. 常見的操作 Stream 實例的方法有以下幾種:

  1. distinct
  2. filter
  3. map
  4. flatMap
  5. peek
  6. limit
  7. skip

distinct 用於去除流中的重複元素, 實際效果如下:

// distinct 方法, 過濾出流中不同的元素, 依賴對象的equals方法
// 輸出結果爲 a c b null 即去除了重複元素
Stream.of("a", "c", "a", "b", "b", null, null)
		.distinct()
		.forEach(System.out::println);

filter 用於自定義過濾規則, 接收一個 Predicate 斷言類型參數, 這個參數符合接收一個元素, 符合要求返回 true, 否則返回 false, 返回 true 則當前元素會保留在新的流實例中, 實際效果如下:

// filter 對 Stream 進行自定義過濾, 參數是一個 Predicate 斷言類型
// 下方篩選出不同元素, 並且值不是b的元素, 注意 Stream 的鏈式調用
// 輸出結果爲 a c null
Stream.of("a", "c", "a", "b", "b", null, null)
        .distinct()
        .filter(str -> str != "b")
        .forEach(System.out::println);

map 用於對元素1對1的轉換, 如將枚舉實例轉換成對應的code, 實際效果如下:

// map 方法將 Stream 實例內容一對一的做轉換或處理
// 接收參數是一個 Function 類型, 接收一個參數並且返回一個處理結果
// 下方將元素依次加1, 輸出結果爲 2 4 6
Stream.of(1, 3, 5)
		.map(num -> num + 1)
		.forEach(System.out::println);

flatMap 用於將流中的容器元素扁平化, 例如流中有2個 List 元素, 每個 List 元素又包含2個 String 元素, 調用 flatMap 方法後得到新的流實例中有4個 String 元素, 即將多餘的List 元素去除了. 底層是通過將每個元素轉換成流實例, 最後再合併成一個流實例實現的.

// flatMap 方法用於取出所有容器類元素包含的元素, 即去除了一層容器
// 接收參數爲一個 Function, 作用爲接收一個參數, 返回一個 Stream 實例
// 就是在將每個元素轉換爲流實例的過程中去除了容器類
// 最後返回結果將所有的中間流對象合併成一個, 實現了將多餘容器扁平化的效果
// 下方代碼輸出 List One List Two 4個 String 元素
Stream.of("List,One", "List,Two")
        .map(str -> str.split(","))
        .flatMap(Arrays::stream)
        .forEach(System.out::println);
// 沒有調用 flatMap 時, 輸出結果爲
// [Ljava.lang.String;@6108b2d7 [Ljava.lang.String;@1554909b
// 兩個字符串數組, 即流實例中有兩個字符串數組元素, 並沒有扁平化成容器中具體元素
Stream.of("List,One", "List,Two")
        .map(str -> str.split(","))
        .forEach(System.out::println);

接下來用綜合示例演示 peek limit skip 的用法

// 輸出結果爲 元素2被消費 元素4被消費 最終剩下的元素4
Stream.of(1, 3, 3, 5)
        .distinct() // 篩選出所有不同的元素 當前值爲 1, 3, 5
        .map(num -> num + 1) // 將所有元素依次自加一, 執行後值爲 2, 4, 6 (當前邏輯未被執行, 最後一處調用才都會執行)
        .peek(num -> log.info("元素{}被消費", num))  // 生成新的 Stream 實例, 實例元素被消費將會調用此處指定的 Consumer
        .skip(1) // 跳過第一個元素, 當前值爲 4, 6
        .limit(1) // 限制只保留前一個元素, 當前值爲 4
        .forEach(num -> log.info("最終剩下的元素{}", num));
Stream實例操作結果的聚集

在上一部分中我們已經瞭解如何處理 Stream 實例中的元素, 但是處理後的結果也是一個流實例, 使用過程還需要最後一步, 即聚集流實例中的元素.
流實例元素聚集方法有兩類, 一種是 collect 將流實例中的元素聚集成容器類, 供後續使用, 一種是對流實例中的元素進行統計, 如 reduce count sum, 獲得結果, 而不關心流實例中實際元素.
Stream 提供了兩個 collect 方法, 具體效果如下:

// 通過原始三個參數的 collect 方法聚集Stream
// 原始的collect方法第一個參數是 Supplier, 提供一個容器
// 第二個參數是 BiConsumer, 用於將元素添加到容器
// 第三個參數也是 BiConsummer, 用於併發狀態產生的多個容器最終合併成一個容器
ArrayList<Object> collectWidthOriginalWay = Stream.of(1, 3, 3, 5)
        .distinct()
        .collect(ArrayList::new, List::add, (left, right) -> left.addAll(right));
log.info("collect 結果是否是List, {}", collectWidthOriginalWay instanceof List);
log.info("collect 結果內容爲, {}", JSON.toJSONString(collectWidthOriginalWay));
log.info("使用 Collectors 提供的收集器作爲 collect 方法參數, 聚集 Stream 元素");
// Collectors 中 toList toSet 收集器比較常用
// 此處展示的收集器是獲取結果, 而不是聚集所有元素
Optional<Integer> max = Stream.of(1, 3, 3, 5)
        .distinct()
        .collect(Collectors.maxBy((num1, num2) -> num1 - num2));
max.ifPresent(result -> log.info("使用 Collectors 的 maxBy 收集器, 收集到的最大值爲{}", result));

接下來展示三種 reduce 的用法

log.info("使用 reduce 對元素進行聚集");
// 只有一個參數的 reduce 方法, 接收一個 BinaryOperator 二元操作符, 其繼承自 BiFunction 接收兩個參數, 返回一個結果
Optional<Integer> total = Stream.of(1, 3, 3, 5)
        .reduce((num1, num2) -> num1 + num2);
total.ifPresent(result -> log.info("一個參數的 reduce 方法獲得的元素和位{}", result));
Integer reduceWidthInitialValue = Stream.of(1, 3, 3, 5).reduce(5, (num1, num2) -> num1 + num2);
log.info("指定初始值進行 reduce, 獲得結果爲{}", reduceWidthInitialValue);
// 三個參數的 reduce 方法進行數據聚集, 上方 reduce 基礎上新增了第三個參數 BinaryOperator
// BinaryOperator 二元操作符, 用於將多個匯聚結果合併成一個結果, 多線程操作會用到第三個參數
Integer reduceWidthThreeParam = Stream.of(1, 3, 3, 5).parallel().reduce(0, (num1, num2) -> num1 + num2, (num1, num2) -> num1 + num2);
log.info("三個參數 reduce 求和結果爲{}", reduceWidthThreeParam);

Stream 操作的性能驗證

網絡上有很多危言聳聽的博文說 Stream 的效率比普通循環低了十幾倍, 這種觀點是錯誤的. 我們實際來驗證 Stream 實際性能, 在1億個隨機數中分別用普通for循環, stream, parallelStream三種方式求最大值三次, 對比消耗時間, 代碼如下:

// 首先準備一個工具方法和三個排序方法
// 生成隨機數
private List<Integer> getIntegers() {
    long curr = System.currentTimeMillis();
    List<Integer> integers = new ArrayList<>();
    Random random = new Random();
    for (int i = 0; i < NUMBER; i++) {
        integers.add(random.nextInt());
    }
    log.info("{}萬個隨機數生成完畢, 耗時{}毫秒", NUMBER / 10000, System.currentTimeMillis() - curr);
    return integers;
}

// 普通for循環尋找最大值
private void normalSort(List<Integer> integers) {
    long fCurr = System.currentTimeMillis();
    Integer fMax = integers.get(0);
    for (Integer integer : integers) {
        fMax = Integer.max(fMax, integer);
    }
    log.info("{}萬個隨機數中,for循環求最大值爲{}, 耗時{}毫秒", NUMBER / 10000, fMax, System.currentTimeMillis() - fCurr);
}

// stream尋找最大值
private void streamSort(List<Integer> integers) {
    long curr = System.currentTimeMillis();
    Integer max = integers.stream().reduce(Integer::max).orElseThrow(NumberFormatException::new);
    log.info("{}萬個隨機數中,流式求最大值爲{}, 耗時{}毫秒", NUMBER / 10000, max, System.currentTimeMillis() - curr);
}

// parallelStream尋找最大值
private void parallelStreamSort(List<Integer> integers) {
    long curr2 = System.currentTimeMillis();
    Integer max2 = integers.parallelStream().reduce(Integer::max).orElseThrow(NumberFormatException::new);
    log.info("{}萬個隨機數中,並行流式求最大值爲{}, 耗時{}毫秒", NUMBER / 10000, max2, System.currentTimeMillis() - curr2);
}

@Test
public void testGetMax() {
    List<Integer> integers = getIntegers();
    for (int i = 0; i < 3; i++) {
        log.info("第{}次排序測試", i);
        this.normalSort(integers);
        this.streamSort(integers);
        this.parallelStreamSort(integers);
    }
}

輸出結果爲

10000萬個隨機數生成完畢, 耗時43355毫秒
第0次排序測試
10000萬個隨機數中,for循環求最大值爲2147483595, 耗時3276毫秒
10000萬個隨機數中,流式求最大值爲2147483595, 耗時1097毫秒
10000萬個隨機數中,並行流式求最大值爲2147483595, 耗時785毫秒
第1次排序測試
10000萬個隨機數中,for循環求最大值爲2147483595, 耗時800毫秒
10000萬個隨機數中,流式求最大值爲2147483595, 耗時735毫秒
10000萬個隨機數中,並行流式求最大值爲2147483595, 耗時807毫秒
第2次排序測試
10000萬個隨機數中,for循環求最大值爲2147483595, 耗時562毫秒
10000萬個隨機數中,流式求最大值爲2147483595, 耗時729毫秒
10000萬個隨機數中,並行流式求最大值爲2147483595, 耗時658毫秒

可以看到第0次求最大值時, 普通for循環消耗的時間高達3.276秒, 將 Stream 或者 parallelStream 求最大值放在第一位也一樣消耗大量時間, 因爲此時虛擬機在進行預熱或者說是優化, 如果是 parallelStream 還涉及到初始化線程池, 第0次驗證並不能說明性能問題. 第1次和第2次求最大值中可以看到三種方式執行時間都在 500 - 800毫秒之間, 需要注意實際執行情況跟執行環境硬件因素也是有很大關係的. 從上面的結果來說, 基本上可以驗證Stream 與原始for循環性能差距並不大, 是可以放心使用 Stream 的.

總結

流利使用 Stream 的前置條件是熟悉 JDK 提供的常用函數式接口. Stream 使用的關鍵點在於 流實例創建 流的處理 處理結果的聚集 三部分.

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