1. 程式人生 > >Java8新特性之Stream

Java8新特性之Stream

前言

在想很好了解 Stream 之前,很有必要簡單的瞭解下函式式變成以及Lambda的概念,可以閱讀另外一篇

Java8新特性之Lambda

大家回憶下日常學習工作中使用的最多的 Java API 是什麼?相信很多人的答案和我一樣都是集合。我們選擇適合的集合資料結構儲存資料,而我們之於集合最多的操作就是遍歷,實現查詢,統計,過濾,合併等業務。

哪裡用Stream

集合迭代

外部迭代:通過 for迴圈,Iterator迭代器遍歷集合,手動的拿到集合中每個元素進行相應處理

  • 優點
    • 對於程式的掌控更高
    • 效能強(如果演算法功力深厚)
  • 缺點
    • 很多重複的模板程式碼
    • 需要很多中間臨時變數來減少遍歷次數
    • 效能完全取決於程式設計師水平,燒腦
    • 程式碼不易讀
    • 容易出錯:例如for迴圈遍歷LinkedList會出錯

內部迭代:只提供對集合中元素的處理邏輯,遍歷過程交給庫類,Java5提供了foreach,Java8提供了Stream

  • 優點
    • 程式碼好讀
    • 簡單,只需要提供處理邏輯
  • 缺點
    • 有些情況效能比外部迭代差一點點
    • 在使用foreach時不能對元素進行賦值操作

為什麼要Stream

本文要介紹的Stream屬於內部迭代,之前我們已經有了foreach減少了我們的程式碼量,為什麼我們還需要Stream呢?

  • 流水線的方式處理集合,結合Lambda爽歪歪
  • 程式碼超短超好讀
  • Stream的開始到結束就相當於一次遍歷,允許我們在遍歷中拼接多個操作
  • 強調的是對集合的計算邏輯,邏輯可以多次複用
  • 特別繁重的任務可以很輕鬆的轉為並行流,適合類似於大資料處理等業務
  • 成本非常小的實現並行執行效果

怎麼用Stream

上文我們大概知道了Stream主要服務與集合,流的過程可以總結為三步:

  • 開始操作:讀取資料來源(如集合)
  • 中間操作:組裝中間操作鏈,形成一條流的流水線
  • 終端操作:一次迭代執行流水線,結束流,並生成結果

中間操作和終端操作

第一步流載入集合資料,庫類完成我們不需要關心,要想使用好Stream有必要從不同維度瞭解主要的操作

  • 中間操作:流水線的部件,返回的是this,也就是Stream,此時迭代並沒有執行
  • 終端操作:流水線真正開始執行,返回的是處理結果,終端操作過後流關閉
  • 無狀態操作:例如對集合中所有元素做轉換,或者過濾,不用儲存別的元素的處理結果
  • 有狀態操作:例如排序,去重操作,需要儲存之前集合中元素的狀態
  • 非短路操作:按部就班完成迭代,返回處理結果
  • 短路操作:只有一達到預設的條件,立刻停止並返回

下圖給出了Stream給出的一些常用api的基本資訊

基本資料型別流

為了避免自動裝箱拆箱消耗效能,Stream為我們提供了IntStream、DoubleStream和LongStream,分別將流中的元素特化為int、long和double,這些特殊的流中提供了range,sum,max等數字型別常用的api

並行流

當我們面對計算是否密集的應用開發時,為了充分利用硬體資源,可以簡單的通過改變parallel方法將流變成並行執行,但是在使用時有如下注意事項。

  • 並行流是通過 fork/join 執行緒池來實現的,該池是所有並行流共享的。預設情況,fork/join 池會為每個處理器分配一個執行緒。假設你有一臺16核的機器,這樣你就只能建立16個執行緒。而與此同時其他任務將無法獲得執行緒被阻塞住,所以使用並行流要結合機器和業務場景。
  • 避免在並行流中改變共享狀態,小心使用有狀態的操作
  • 並行流並不一定就快,要將多個執行緒的執行結果彙總

實戰

相信大家最關心的還是實際開發中能幫助我們解決哪些問題,通過一些簡單案例熟悉各種操作的用法

開始操作

生成空流

Stream<Object> empty = Stream.empty();

值生成流

Stream<String> stringStream = Stream.of("1", "2", "3");

陣列生成流

String[] strings = { "1", "2", "3"};
Stream<String> stream = Arrays.stream(strings);

集合生成流

List<String> strings1 = Arrays.asList("1", "2", "3");
Stream<String> stream1 = strings1.stream();

檔案生成流

Stream<String> lines = Files.lines(Paths.get("/c/mnt/"));

函式生成流(無限流)

// 無限流,流從0開始,下面的每個元素依次加2
Stream<Integer> iterate = Stream.iterate(0, num -> num + 2);
// 無限流,流中每個元素都是 0~1 隨機數
Stream<Double> generate = Stream.generate(Math::random);

數值範圍生成流

// 生成0到10的int流
IntStream intStream = IntStream.rangeClosed(0, 10);
// 生成0到9的int流
IntStream intStream1 = IntStream.range(0, 10);

手動生成流

// 生成有字串a和數字1的異構的流
Stream.builder().add("a").add(1).build().forEach(System.out::print);

合併兩個流

Stream.concat(
    Stream.of("1", 22, "333"),
    Stream.of("1", 22, 333)
).forEach(System.out::print);

中間操作

在介紹中間操作時,為了方便學習演示使用到了終端操作中的foreach方法,作用就和我們寫的foreach迴圈類似,遍歷執行,不返回值。

過濾filter

// 過濾所有空字串,注意是返回 true的留下
Stream.of("1", null, "2", "", "3")
    .filter(StringUtils::isNotEmpty)
    .forEach(System.out::print);

去重distinct

// 去掉重複的2
Stream.of("1", "2", "2", "2", "3")
    .distinct()
    .forEach(System.out::print);

跳過skip

// 跳過前兩個元素
Stream.of("1", "2", "3", "4", "5")
    .skip(2)
    .forEach(System.out::print);

截短limit

// 與skip相反,只留下前2個
Stream.of("1", "2", "3", "4", "5")
    .limit(2)
    .forEach(System.out::print);

對映map

@Data
@AllArgsConstructor
public class Person {
    private String name;
}

// 將流中每個字串轉為Person例項
Stream.of("1", "2", "3")
    .map(Person::new)
    .forEach(System.out::print);

// 將流每個字串變成其長度,為了避免自動拆箱,使用mapToInt轉為IntStream
Stream.of("1", "2", "3")
    .mapToInt(String::length)
    .forEach(System.out::print);

扁平對映flatMap

用法和map類似,當我們需要把流中的每個元素全對映另外一個流,也就是資料在流中流裡面,這時候操作就不方便,藉助flatMap,我們可以把所有第二層流中的元素合併到最外層流

// 每個字串按照逗號分隔合併成一個流
Stream.of("1,2,3", "4,5,6", "7,8,9")
    .flatMap(a -> Stream.of(a.split(",")))
    .forEach(System.out::print);

排序sorted

Stream.of("4", "3", "5")
    .sorted()
    .forEach(System.out::print);

Stream.of("4", "3", "5")
    .sorted(Comparator.naturalOrder())
    .forEach(System.out::print);

並行流相關parallel,sequential,unordered

// 序列流轉並行流
Stream.of("1", "2", "3").parallel();
// 並行流轉序列流
Arrays.asList("1", "2", "3").parallelStream().sequential();

// 在並行流中加上unordered,使得流變成無序,提供並行效率,此時limit相當於隨機取2個元素
Arrays.asList("1", "2", "3", "4", "5")
    .parallelStream()
    .unordered()
    .limit(2).forEach(System.out::print);

除錯peek

用於debug除錯程式碼,在每次執行操作前,看一眼元素

// 每個元素會列印兩遍
Stream.of("1", "2", "3").peek(System.out::print).forEach(System.out::print);

終端操作

遍歷foreach

Stream.of("1", "2", "3").forEach(System.out::print);

// 並行流中使用用於保持有序
Arrays.asList("1", "2", "3").parallelStream().forEachOrdered(System.out::print);

通用統計count,max,min

// 總個數,返回true|false
Stream.of("1", "2", "3").count();
// 最大元素,返回Optional
Stream.of("1", "2", "3").max(Comparator.naturalOrder());
// 最小元素,返回Optional
Stream.of("1", "2", "3").min(Comparator.naturalOrder());

數值流特有統計sum,average,summaryStatistics

以 IntStream 舉例

// 累加求和
Stream.of("1", "2", "3").mapToInt(Integer::valueOf).sum();
// 求平均數
Stream.of("1", "2", "3").mapToInt(Integer::valueOf).average();
// 總和,最大值,最小值,平均數,總個數,應有盡有
Stream.of("1", "2", "3").mapToInt(Integer::valueOf).summaryStatistics();

匹配match

返回true|false

// 是否有長度大於 2 的字串
Stream.of("1", "22", "333").anyMatch(s -> s.length() > 2);
// 是否一個長度大於 2 的字串也沒有
Stream.of("1", "22", "333").noneMatch(s -> s.length() > 2);
// 是否字串長度全大於 2
Stream.of("1", "22", "333").allMatch(s -> s.length() > 2);

查詢find

由於是短路操作,所以只有在序列流中findAny和findFirst才區別明顯

// 找到流中任意一個元素,普通流一般也返回第一個元素,並行流中返回任意元素
Stream.of("1", "22", "333").findAny();
// 找到流中第一個元素,普通流和並行流都一樣
Stream.of("1", "22", "333").findFirst();

彙總collect

把所有處理結果彙總,Collectors收集器裡提供了很多常用的彙總操作

// 將結果彙總成一個list
Stream.of("1", "22", "333").collect(Collectors.toList());

歸約reduce

谷歌著名的map-reduce理想,用於最後彙總結果

// 0作為起始的pre,流的結果等於pre乘以自身再加一,直到curr到達最後一個元素
// (1) pre = 0 curr = 1 計算 pre = 0 * 1 + 1 = 1 
// (2) pre = 1 curr = 2 計算 pre = 1 * 2 + 1 = 3
// (3) pre = 3 curr = 3 計算 pre = 3 * 3 + 1 = 10
// (4) pre = 10 curr = null 返回結果 10
IntStream.of(1, 2, 3).reduce(0, (pre, curr) -> pre * curr + 1);

// 當然如果不設定初始值,流中第一個元素就是pre
IntStream.of(1, 2, 3).reduce((pre, curr) -> pre * curr + 1);

轉陣列toArray

Stream.of("1", "22", "333").toArray();

獲得迭代器iterator

Stream.of("1", "2", "3").iterator();

獲得並行可分迭代器spliterator

Stream.of("1", "2", "3").spliterator();

流型別判斷isParallel

Stream.of("1", "2", "3").isParallel();

收集器進階

在介紹 collect 操作中,我們用了 Collectors 中提供的 toList 方法將結果彙總成List,collect 是很常用的操作Collectors中有很多有用的方法值得熟悉一下,其實很多終端方法都是 collect 的快捷寫法,如果都不能滿足需求我們還可以自己實現一個

轉常用集合

// 轉list
List<String> collect = Stream.of("1", "2", "3").collect(Collectors.toList());

// 轉set
Set<String> collect = Stream.of("1", "2", "3").collect(Collectors.toSet());

// 轉map,key為字串長度,value為字串本身
Map<Integer, String> collect = Stream.of("1", "2", "3")
    .collect(Collectors.toMap(String::length, Function.identity()));

// 轉併發版map,key為字串長度,value為字串本身
Map<Integer, String> collect = Stream.of("1", "2", "3")
    .collect(Collectors.toConcurrentMap(String::length, Function.identity()));

// 轉指定型別集合
ArrayList<String> collect = Stream.of("1", "2", "3")
    .collect(Collectors.toCollection(ArrayList::new));

拼接字串

// 拼接成一個字串
String collect = Stream.of("1", "2", "3").collect(Collectors.joining());

// 拼接成一個字串,逗號分隔
String collect = Stream.of("1", "2", "3").collect(Collectors.joining(","));

統計

都有對應簡化版,一些更加靈活多變的操作可以用Collectors

// 和終端操作中的 max 等價
Stream.of("1", "2", "3").collect(Collectors.maxBy(Comparator.naturalOrder()));

// 和終端操作中的 min 等價
Stream.of("1", "2", "3").collect(Collectors.minBy(Comparator.naturalOrder()));

// 和終端操作中的 count 等價
Stream.of("1", "2", "3").collect(Collectors.counting(Integer::valueOf));

// 和數值流終端操作中的 sum 等價
Stream.of("1", "2", "3").collect(Collectors.summingInt(Integer::valueOf));

// 和數值流終端操作中的 average 等價
Stream.of("1", "2", "3").collect(Collectors.averagingInt(Integer::valueOf));

// 和數值流終端操作中的 summaryStatistics 等價
Stream.of("1", "2", "3").collect(Collectors.summarizingInt(Integer::valueOf));

分組

可以和其他收集器方法任意組合

// 以字串長度分成3組,map的key為長度,value為對應長度字串list
Map<Integer, List<String>> collect = Stream.of("1", "22", "33", "4", "555")
    .collect(Collectors.groupingBy(String::length));

// 以字串長度分成3組,map的key為長度,value為對應的元素個數
Map<Integer, Long> collect = Stream.of("1", "22", "33", "4", "555")
    .collect(Collectors.groupingBy(String::length, Collectors.counting()));

// 這個例子沒有實際意義,展示我們可以進行二級分組,長度分組完畢,再組內以hash值分組
Map<Integer, Map<Integer, List<String>>> collect = Stream.of("1", "22", "33", "4", "555")
    .collect(Collectors.groupingBy(
        String::length,
        Collectors.groupingBy(String::hashCode)
    ));

分割槽

分割槽時特殊的分組,使用方法類似,特殊是通過謂詞表達式只能分成兩組,true是一組,false是一組

// 以長度大於2為標準分割槽
Map<Boolean, List<String>> collect = Stream.of("1", "22", "33", "4", "555")
    .collect(Collectors.partitioningBy(s -> s.length() > 2));

歸約

// 和終端操作 reduce 等價
Stream.of("1", "22", "33", "4", "555")
    .collect(Collectors.reducing(0, Integer::valueOf, Integer::sum))

多操作連線

// 把流彙總成list,然後再求出其容量
Integer collect = Stream.of("1", "22", "33", "4", "555")
    .collect(Collectors.collectingAndThen(Collectors.toList(), List::size));

自定義

有些時候預設的實現有缺陷,或者追求更高的效能我們需要自己實現收集器。只要實現 Collector<T, A , R>介面 中的方法我們就可以獲得自己的收集器,其中 T 是元素泛型,A是累加器結果,R是最終返回結果,所有首先我們來看下要實現哪些方法

  • supplier:提供一個容器 A 裝結果
  • accumulator:累加器,將元素累加進剛才建立的容器
  • combiner:合併容器的結果
  • finisher:完成操作,將 A 轉為 R 返回
  • characteristics:是個定義標識的方法
    • UNORDERED:結果不受流中專案的遍歷和累積順序的影響
    • CONCURRENT:accumulator函式可以從多個執行緒同時呼叫
    • IDENTITY_FINISH:表示 finisher 沒做任何事情,直接返回了累加的結果,也就是A和R相同
public static final Collector<String, List<String>, List<String>> myToList = Collector.of(
    // supplier: 建立 A(ArrayList)
    ArrayList::new,
    // accumulator:把每個元素放入 A 中
    (list, el) -> list.add(el),
    // combiner:如果並行拆分成多個流,直接 addAll 合併
    // 如果不想支援並行可以寫個空,或拋UnsupportedOperationException異常
    (listA, listB) -> {
        listA.addAll(listB);
        return listA;
    },
    // finisher:不做任何事情,直接返回 A
    Function.identity(),
    // characteristics...:表示 A R 型別相同, 且支援並行流
    Collector.Characteristics.IDENTITY_FINISH,
    Collector.Characteristics.CONCURRENT
);

// 自定義收集器轉成list
List<String> collect = Stream.of("1", "22", "33", "4", "555").collect(myToList);