還在用迭代器處理集合嗎?試試Stream,真香

後青春期的Keats發表於2020-05-07

前言

上一篇部落格一文帶你深入瞭解 Lambda 表示式和方法引用我給大家介紹了 Java8 函式式特性中的 Lambda,這篇文章我將繼續討論 stream 流的用法

宣告:本文首發於部落格園,作者:後青春期的Keats;地址:https://www.cnblogs.com/keatsCoder/ 轉載請註明,謝謝!

Show Time

首先給大家看一段程式碼,讓大家直觀感受下 Java7 和 Java8 遍歷處理集合的不同

Dish 是一個菜餚物件,calories 屬性表示該菜品的卡路里值,name 則是菜品的名稱。我們需要過濾出卡路里小於400、然後根據卡路里值升序、接著拿到他們的名稱列表並返回

Java7

public static List<String> getLowCaloricDishesNamesInJava7(List<Dish> dishes){
    List<Dish> lowCaloricDishes = new ArrayList<>();
    for(Dish d: dishes){
        if(d.getCalories() < 400){
            lowCaloricDishes.add(d);
        }
    }
    List<String> lowCaloricDishesName = new ArrayList<>();
    Collections.sort(lowCaloricDishes, new Comparator<Dish>() {
        public int compare(Dish d1, Dish d2){
            return Integer.compare(d1.getCalories(), d2.getCalories());
        }
    });
    for(Dish d: lowCaloricDishes){
        lowCaloricDishesName.add(d.getName());
    }
    return lowCaloricDishesName;
}

Java8

public static List<String> getLowCaloricDishesNamesInJava8(List<Dish> dishes){
    return dishes.stream()
        .filter(d -> d.getCalories() < 400)
        .sorted(comparing(Dish::getCalories))
        .map(Dish::getName)
        .collect(toList());
}

如果需要多核並行處理,則只需呼叫 dishes.parallelStream() 即可

在 Java8 之前,程式設計師需要通過 2次遍歷 + 一次集合排序才能完成的工作,Java8 只需要一個鏈式呼叫就可以解決。這就是 stream 的強大之處

img

認識流

流是什麼

流是 Java API 的新成員,允許程式設計師以宣告式的方式處理集合資料,並且支援鏈式呼叫、支援並行處理。用流處理的集合資料高效且易讀。

流與集合的異同

  1. 集合的主要功能是以一定的時間和空間複雜度儲存和訪問元素,而流主要是用於元素計算
  2. 集合中的元素可以隨意新增和刪除,而流不能新增和刪除元素
  3. 流的元素是按需計算的,只有當用到時他才會參與計算,而集合中的元素必須提前全都準備好
  4. 流只能遍歷一次,下面的程式碼會報錯 java.lang.IllegalStateException: stream has already been operated upon or closed 流已經被消費掉
List<String> names = Arrays.asList("Java8", "Lambdas", "In", "Action");
Stream<String> s = names.stream();
s.forEach(System.out::println);
s.forEach(System.out::println);
  1. 集合採用外部迭代,流採用內部迭代。內部迭代意味著 Java 可以替你選擇更優的迭代策略和並行處理。而外部迭代如果程式設計師想著做個更有的迭代/採用並行就相當於“下次一定”?了

流操作分類

對流的操作可以分為兩類,可以繼續執行下一個流操作的稱為中間操作(方法的返回值是 Stream),關閉流的操作稱為終止操作。

中間操作

除非流水線上執行終端操作,否則中間操作不會執行任何處理。流會對中間操作進行合併、短路等優化

終端操作

終端操作會從流的流水線生成結果,返回一個非 Stream 的任意型別值

使用流

篩選和切片

篩選

filter(Predicate<? super T> predicate) 方法可以將流中滿足某條件的元素篩選出來。該方法接收一個謂詞函式,返回流。比如要選出某個蘋果集合中紅色的蘋果

List<Apple> appleList = new ArrayList<>();
List<Apple> redAppleList = appleList.stream().filter(a -> "red".equals(a.getColor())).collect(Collectors.toList());

去重

distinct() 方法會根據元素的 hashCode() 和 equals() 方法對流中元素進行去重操作

截斷

limit(n) 方法會返回流的前 n 個元素,對於有序集合List,流會按照新增順序返回前 n 個元素,而無序集合則不會

跳過

skip(n) 方法會跳過流的前 n 個元素,可以通過 skip(m).limit(n) 返回列表中第 m - (m+n) 區間的元素,類似與 mysql 中的 limit m,n

對映

對流中的每個元素應用函式

map(Function<? super T, ? extends R> mapper) 方法。該方法接收一個 Function 函式,對流中的每一個元素使用。然後可以返回任意型別的物件。有了該方法,就可以結合 Lambda 表示式對集合中的元素使用函式進行各種轉換

流的扁平化

flatMap() 可以將流操作中多個流合併成一個流的多個元素。舉個例子:集合 words 有兩個單詞,現在想獲得[H, e, l, o, W, r, d] 在 split 方法執行完畢後,返回的是 Stream(String[]) 物件,而此時如果執行 map 方法,返回的就是多個流的集合(這個例子中就是兩個 Stream(String)),這時是無法繼續接下來的 distinct 操作的,因此需要 flatMap 將兩個 Stream 扁平化成一個 Stream,然後進行操作

List<String> words = Arrays.asList("Hello", "World");

List<String> charList = words.stream().map(word -> word.split("")).flatMap(Arrays::stream).distinct().collect(Collectors.toList());

該方法的方法宣告 flatMap(Function<? super T, ? extends Stream<? extends R>> mapper) 中可以看出,他所使用的函式式介面 Function 第二個泛型 R 必須是 Stream 流。即函式式介面的抽象方法返回值必須是 Stream 流及其子類物件。

查詢和匹配

檢查謂詞是否至少匹配一個元素

anyMatch 方法可以回答“流中是否存在至少一個複合謂詞條件的元素”返回 boolean 型別的值,因此是一個終端操作,例如

List<Integer> num = Arrays.asList(1, 2, 3, 4, 5, 6);

if (num.stream().anyMatch(n -> n % 3 == 0)) {
    System.out.println("集合中有元素是3的整數倍");
}

控制檯會輸出'集合中有元素是3的整數倍',因為集合中 3、6都是3的整數倍,符合謂詞的條件

檢查謂詞是否匹配所有元素

allMatch 方法和 anyMatch 方法原理類似,但是它僅當所有元素滿足謂詞條件時,返回 true。

noneMatchallMatch 正好相反,僅當所有元素不滿足謂詞條件時,返回 true

ps:和 && || 運算子類似,以上三個操作都用到了短路的思想來提高效率。

查詢元素

findAny() 該方法返回當前流中的任意元素,可以和其他流操作結合使用,這裡需要注意 findAny() 返回的結果被 Optional 所包裹,Optional 是 Java8 為優雅的避免 NPE 所採用的新 API,關於 Optional 的用法我會在下一篇部落格和大家討論,敬請期待。這裡需要說明的就是 Optional.ifPresent(Consumer<? super T> consumer) 表示當 Optional 包裹的元素不為空時,執行 consumer

num.stream().filter(n -> n > 2).findAny().ifPresent(System.out::println);

findFirst() 該方法返回當前流中的第一個元素,一般也和其他流操作(例如 filter() 過濾)結合使用。與 findAny() 不同的是,他一定返回有序集合的第一個滿足條件的元素。當然有得必有失,作為代價,findFirst() 在並行處理時限制更多一些。

歸約

元素求和

reduce(T identity, BinaryOperator<T> accumulator); 方法接收兩個引數:identity 初始值,accumulator 對兩個數的操作。例如求集合中數字的和:

num.stream().reduce(0, (a, b) -> a + b) // 計算完成,返回 21

ps:Lambda 表示式 (a, b) -> a + b) 中 a 是上一輪執行完後的累計值,b 是本次迴圈流中的元素。通過累加就可以計算出數字的和

最大值和最小值

reduce 方法不僅可以求和、求積。甚至可以計算最大值、最小值。

num.stream().reduce(Integer::max);
num.stream().reduce(Integer::min);

總結

  1. 流是 Java API 的新成員,允許程式設計師以宣告式的方式處理集合資料,並且支援鏈式呼叫、支援並行處理。用流處理的集合資料高效且易讀。
  2. 流的API中可以分為兩大類,中間操作和終端操作,中間操作返回流物件,可以鏈式呼叫,終端操作則返回非流物件。
  3. 流提供了很多方便的API,如篩選 filter、去重 distinct、截斷 limit、跳過 skip、函式轉換 map、扁平化 flatMap、判斷流中是否有任意元素符合要求 anyMatch、是否所有元素都符合要求 allMatch、是否所有元素都不符合要求 noneMatch、查詢元素 findAny findFirst、累計式的計算元素 reduce

碼字不易,如果你覺得讀完以後有收穫,不妨點個推薦讓更多的人看到吧!

相關文章