Java8的stream流讓操作集合更容易

꧁ʚ星月天空ɞ꧂發表於2024-06-17

概述

好久不見,最近忙於工作,好久沒有發文章了,入職大公司,發現有些同事更喜歡使用stream流操作集合,故而自己也研究學習一下。事先宣告:我並非原創,我只是學習並整理的大佬們的文章,原文章放在最後,有興趣的可以去看看
Java8提供了Stream(流)處理集合的關鍵抽象概念,Stream 使用一種類似用 SQL 語句從資料庫查詢資料的直觀方式來提供一種對 Java 集合運算和表達的高階抽象。它可以對集合進行操作,可以執行非常複雜的查詢、過濾和對映資料等操作。Stream API 藉助於同樣新出現的Lambda表示式,極大提高Java程式設計師的生產力,讓程式設計師寫出高效率、乾淨、簡潔的程式碼。

stream流的三個過程

  • 建立Stream
  • Stream中間處理
  • 終止Steam
    image
  1. 開始管道
    主要負責新建一個Stream流,或者基於現有的陣列、List、Set、Map等集合型別物件建立出新的Stream流。
    image

  2. 中間管道
    負責對Stream進行處理操作,並返回一個新的Stream物件,中間管道操作可以進行疊加。
    image

  3. 終止管道
    顧名思義,透過終止管道操作之後,Stream流將會結束,最後可能會執行某些邏輯處理,或者是按照要求返回某些執行後的結果資料。
    image

常用示例

開始管道

  1. 透過集合
 public static List<Employee> getEmployeeDataList(){
    List<Employee> list = new ArrayList<>();
     list.add(new Employee(1,"張三",20,8500D,1));
     list.add(new Employee(2,"李四",18,600D,1));
     list.add(new Employee(3,"王五",21,5500D,3));
     list.add(new Employee(4,"小白",30,8500D,2));
     return list;
 }

public static void main(String[] args) {
    List<Employee> employees = getEmployeeDataList();
    // 返回一個順序流 (按照集合順序獲取)
    Stream<Employee> stream = employees.stream();
    // 返回一個並行流 (類似於執行緒去獲取資料,無序)
    Stream<Employee> parallelStream = employees.parallelStream();
}

  1. 透過陣列
public static void main(String[] args) {
    int[] arr = new int[]{1,2,3,4,5,6};
    IntStream intStream = Arrays.stream(arr);

    Employee e1 = new Employee(1, "張三", 20, 8500D, 1);
    Employee e2 = new Employee(2, "李四", 18, 600D, 1);
    Employee[] employees = new Employee[]{e1,e2};
    Stream<Employee> stream = Arrays.stream(employees);
}

  1. 透過Stream的of方法
public static void main(String[] args) {
 Stream<Integer> integerStream = Stream.of(1, 2, 3, 4, 5, 6);
}

  1. 透過無限流
public static void main(String[] args) {
    // 生成偶數
    Stream.iterate(0,t->t+2).limit(10).forEach(System.out::println);
    // 10個隨機數
    Stream.generate(Math::random).limit(10).forEach(System.out::println);
}

中間管道

  1. filter:按照條件過濾符合要求的元素, 返回新的stream流

image

篩選工資大於8000的員工:

public static void main(String[] args) {
    List<Employee> employees = getEmployeeDataList();
    Stream<Employee> stream = employees.stream();
    stream.filter(e -> e.getSalary() > 8000).forEach(t->{
        System.out.println("工資大於八千的員工->>>"+t);
    });
}

image

  1. map:將已有元素轉換為另一個物件型別,一對一邏輯,返回新的stream流

大小寫轉換:

public static void main(String[] args) {
    List<String> list = Arrays.asList("a", "b", "c", "d");
    list.stream().map(str -> str.toUpperCase(Locale.ROOT)).forEach(t-> System.out.println("大小寫轉換->>>"+t));
}

image

獲取員工姓名大於3的員工姓名:
image

public static void main(String[] args) {
    // 獲取員工姓名大於3的員工姓名
    List<Employee> list = getEmployeeDataList();
    Stream<String> nameStream = list.stream().map(Employee::getName);
    nameStream.filter(name -> name.length() > 3).forEach(t-> System.out.println("獲取員工姓名大於3的員工->>>"+t));
}

image

  1. flatMap:將已有元素轉換為另一個物件型別,一對多邏輯,即原來一個元素物件可能會轉換為1個或者多個新型別的元素,返回新的stream流

使用flatMap()將流中的每一個元素連結成為一個流:

public class flatMapTest {
    public static void main(String[] args) {
        //建立使用者列表
        List<String> userList = new ArrayList<String>();
        userList.add("康熙爺、莫愁、顏如玉");
        userList.add("紀曉嵐、杜小月、和珅");
 
        //分割使用者列表,使用flatMap()將流中的每一個元素連結成一個流。
        userList = userList.stream()
                .map(city -> city.split("、"))
                .flatMap(Arrays::stream)
                .collect(Collectors.toList());
 
        //遍歷使用者列表
        userList.forEach(System.out::println);
    }
}

image

  1. limit:僅保留集合前面指定個數的元素,返回新的stream流
  2. skip:跳過集合前面指定個數的元素,返回新的stream流

獲取使用者列表,要求跳過第1條資料後的前3條資料:

public class limitAndSkipTest {
    public static void main(String[] args) {
 
        List<User> userList = new ArrayList<User>();
        userList.add(new User(1, "康熙爺", "男", 32, "總裁辦", BigDecimal.valueOf(3000)));
        userList.add(new User(2, "和珅", "男", 30, "財務部", BigDecimal.valueOf(1800)));
        userList.add(new User(3, "顏如玉", "女", 20, "人事部", BigDecimal.valueOf(1700)));
        userList.add(new User(4, "紀曉嵐", "男", 29, "研發部", BigDecimal.valueOf(2000)));
        userList.add(new User(5, "杜小月", "女", 23, "人事部", BigDecimal.valueOf(1500)));
 
        List<User> limitAndSkipList = userList.stream().skip(1).limit(3).collect(Collectors.toList());
        limitAndSkipList.forEach(System.out::println);
 
    }
}

image

  1. concat:將兩個流的資料合併起來為1個新的流,返回新的stream流
  2. distinct:對Stream中所有元素進行去重,返回新的stream流
public static void main(String[] args) {
    List<Employee> list = new ArrayList<>();
    list.add(new Employee(1,"張三",20,8500D,1));
    list.add(new Employee(1,"張三",20,8500D,1));
    list.add(new Employee(1,"張三",20,8500D,1));
    list.add(new Employee(1,"張三",20,8500D,1));
    list.add(new Employee(1,"張三",20,8500D,1));
    list.stream().distinct().forEach(t-> System.out.println("集合去重->>>"+t));
}

image

  1. sorted:對stream中所有的元素按照指定規則進行排序,返回新的stream流

image

先按照年齡從小到大排序,當年齡一樣的時候,按照工資高低進行排序:

public static void main(String[] args) {
   List<Employee> list = getEmployeeDataList();
   list.stream().sorted((e1,e2)->{
       int age = Integer.compare(e1.getAge(),e2.getAge());
       if(age != 0){
           return age;
       }else {
           return Double.compare(e1.getSalary(),e2.getSalary());
       }
   }).forEach(System.out::println);
}
  1. peek:對stream流中的每個元素進行逐個遍歷處理,返回處理後的stream流

peek方法可以不調整元素順序和數量的情況下消費每一個元素,然後產生新的流,按文件上的說明,主要是用於對流執行的中間過程做debug的時候使用,因為Stream使用的時候一般都是鏈式呼叫的,所以可能會執行多次流操作,如果想看每個元素在多次流操作中間的流轉情況,就可以使用這個方法實現

Stream.of("one", "two", "three", "four")
     .filter(e -> e.length() > 3)
     .peek(e -> System.out.println("Filtered value: " + e))
     .map(String::toUpperCase)
     .peek(e -> System.out.println("Mapped value: " + e))
     .collect(Collectors.toList());
     
輸出:
Filtered value: three
Mapped value: THREE
Filtered value: four
Mapped value: FOUR

終止管道

  1. count:返回stream處理後最終的元素個數
  2. max: 返回stream處理後的元素最大值
  3. min:返回stream處理後的元素最小值
  4. findFirst:找到第一個符合條件的元素時則終止流處理
  5. findAny:找到任何一個符合條件的元素時則退出流處理,這個對於序列流時與findFirst相同,對於並行流時比較高效,任何分片中找到都會終止後續計算邏輯
  6. anyMatch:返回一個boolean值,類似於isContains(),用於判斷是否有符合條件的元素
  7. allMatch:返回一個boolean值,用於判斷是否所有元素都符合條件
  8. noneMatch:返回一個boolean值, 用於判斷是否所有元素都不符合條件
    使用 anyMatch()、allMatch()、noneMatch() 進行判斷:
public class matchTest {
    public static void main(String[] args) {
 
        List<User> userList = new ArrayList<User>();
        userList.add(new User(1, "康熙爺", "男", 32, "總裁辦", BigDecimal.valueOf(3000)));
        userList.add(new User(2, "和珅", "男", 30, "財務部", BigDecimal.valueOf(1800)));
        userList.add(new User(3, "顏如玉", "女", 20, "人事部", BigDecimal.valueOf(1700)));
        userList.add(new User(4, "紀曉嵐", "男", 29, "研發部", BigDecimal.valueOf(2000)));
        userList.add(new User(5, "杜小月", "女", 23, "人事部", BigDecimal.valueOf(1500)));
 
        //判斷使用者列表中是否存在名稱為“杜小月”的資料
        boolean result = userList.stream().anyMatch(user -> user.getName().equals("杜小月"));
 
        //判斷使用者名稱稱是否都包含“杜小月”欄位
        boolean result2 = userList.stream().allMatch(user -> user.getName().equals("杜小月"));
 
        //判斷使用者名稱稱是否存在不包含“杜小月”欄位
        boolean result3 = userList.stream().noneMatch(user -> user.getName().equals("杜小月"));
 
        //列印結果
        System.out.println("result=" + result);
        System.out.println("result2=" + result2);
        System.out.println("result3=" + result3);
 
    }
}

image

  1. collect: 將流轉換為指定的型別,透過Collectors進行指定
  2. toArray:將流轉換為陣列
  3. iterator:將流轉換為Iterator物件
  4. foreach:無返回值,對元素進行逐個遍歷,然後執行給定的處理邏輯

stream細節區分與避坑點

細節區分

  1. map與flatMap

    map與flatMap都是用於轉換已有的元素為其它元素,區別點在於:
    - map 必須是一對一的,即每個元素都只能轉換為1個新的元素
    - flatMap 可以是一對多的,即每個元素都可以轉換為1個或者多個新的元素
    image
    比如:有一個字串ID列表,現在需要將其轉為User物件列表。可以使用map來實現

/**
 * 演示map的用途:一對一轉換
 */
public void stringToIntMap() {
    List<String> ids = Arrays.asList("205", "105", "308", "469", "627", "193", "111");
    // 使用流操作
    List<User> results = ids.stream()
            .map(id -> {
                User user = new User();
                user.setId(id);
                return user;
            })
            .collect(Collectors.toList());
    System.out.println(results);
}

執行之後,會發現每一個元素都被轉換為對應新的元素,但是前後總元素個數是一致的:

[User{id='205'}, 
 User{id='105'},
 User{id='308'}, 
 User{id='469'}, 
 User{id='627'}, 
 User{id='193'}, 
 User{id='111'}]

再比如:現有一個句子列表,需要將句子中每個單詞都提取出來得到一個所有單詞列表。這種情況用map就搞不定了,需要flatMap上場了:


public void stringToIntFlatmap() {
    List<String> sentences = Arrays.asList("hello world","Jia Gou Wu Dao");
    // 使用流操作
    List<String> results = sentences.stream()
            .flatMap(sentence -> Arrays.stream(sentence.split(" ")))
            .collect(Collectors.toList());
    System.out.println(results);
}

執行結果如下,可以看到結果列表中元素個數是比原始列表元素個數要多的:

[hello, world, Jia, Gou, Wu, Dao]

flatMap操作的時候其實是先每個元素處理並返回一個新的Stream,然後將多個Stream展開合併為了一個完整的新的Stream

  1. peek和foreach方法

peek和foreach,都可以用於對元素進行遍歷然後逐個的進行處理:
但根據前面的介紹,peek屬於中間方法,而foreach屬於終止方法。這也就意味著peek只能作為管道中途的一個處理步驟,而沒法直接執行得到結果,其後面必須還要有其它終止操作的時候才會被執行;而foreach作為無返回值的終止方法,則可以直接執行相關操作

public void testPeekAndforeach() {
    List<String> sentences = Arrays.asList("hello world","Jia Gou Wu Dao");
    // 演示點1: 僅peek操作,最終不會執行
    System.out.println("----before peek----");
    sentences.stream().peek(sentence -> System.out.println(sentence));
    System.out.println("----after peek----");
    // 演示點2: 僅foreach操作,最終會執行
    System.out.println("----before foreach----");
    sentences.stream().forEach(sentence -> System.out.println(sentence));
    System.out.println("----after foreach----");
    // 演示點3: peek操作後面增加終止操作,peek會執行
    System.out.println("----before peek and count----");
    sentences.stream().peek(sentence -> System.out.println(sentence)).count();
    System.out.println("----after peek and count----");
}

輸出結果可以看出,peek獨自呼叫時並沒有被執行、但peek後面加上終止操作之後便可以被執行,而foreach可以直接被執行:

----before peek----
----after peek----
----before foreach----
hello world
Jia Gou Wu Dao
----after foreach----
----before peek and count----
hello world
Jia Gou Wu Dao
----after peek and count----
  1. reduce
    reduce 是 Stream 的一個聚合方法,它可以把一個 Stream 的所有元素按照聚合函式聚合成一個結果。reduce 方法傳入的物件是BinaryOperator 介面,它定義了一個 apply 方法,負責把上次累加的結果和本次的元素進行運算,並返回累加的結果。

舉個例子,陣列求和:

Optional<Integer> optional = Stream.of(1, 2, 3, 4, 5).reduce((a, b) -> a + b);
System.out.println(optional);
System.out.println(optional.orElse(-1));

結果如下:

Optional[15]
15 Optinal<T> 物件是一種包裝器物件,它在值不存在的情況下會產生一個可替代物,二隻有在值存在的情況下才會使用這個值。

避坑點

這裡需要補充提醒下,一旦一個Stream被執行了終止操作之後,後續便不可以再讀這個流執行其他的操作了,否則會報錯,看下面示例:

public void testHandleStreamAfterClosed() {
    List<String> ids = Arrays.asList("205", "10", "308", "49", "627", "193", "111", "193");
    Stream<String> stream = ids.stream().filter(s -> s.length() > 2);
    // 統計stream操作後剩餘的元素個數
    System.out.println(stream.count());
    System.out.println("-----下面會報錯-----");
    // 判斷是否有元素值等於205
    try {
        System.out.println(stream.anyMatch("205"::equals));
    } catch (Exception e) {
        e.printStackTrace();
    }
    System.out.println("-----上面會報錯-----");
}

執行的時候,結果如下:

6
-----下面會報錯-----
java.lang.IllegalStateException: stream has already been operated upon or closed
	at java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:229)
	at java.util.stream.ReferencePipeline.anyMatch(ReferencePipeline.java:449)
	at com.veezean.skills.stream.StreamService.testHandleStreamAfterClosed(StreamService.java:153)
	at com.veezean.skills.stream.StreamService.main(StreamService.java:176)
-----上面會報錯-----

結果收集終止方法

因為Stream主要用於對集合資料的處理場景,所以除了上面幾種獲取簡單結果的終止方法之外,更多的場景是獲取一個集合類的結果物件,比如List、Set或者HashMap等。

這裡就需要collect方法出場了,它可以支援生成如下型別的結果資料:

  • 一個集合類,比如List、Set或者HashMap等
  • StringBuilder物件,支援將多個字串進行拼接處理並輸出拼接後結果
  • 一個可以記錄個數或者計算總和的物件(資料批次運算統計)

生成集合

public void testCollectStopOptions() {
    List<Dept> ids = Arrays.asList(new Dept(17), new Dept(22), new Dept(23));
    // collect成list
    List<Dept> collectList = ids.stream().filter(dept -> dept.getId() > 20)
            .collect(Collectors.toList());
    System.out.println("collectList:" + collectList);
    // collect成Set
    Set<Dept> collectSet = ids.stream().filter(dept -> dept.getId() > 20)
            .collect(Collectors.toSet());
    System.out.println("collectSet:" + collectSet);
    // collect成HashMap,key為id,value為Dept物件
    Map<Integer, Dept> collectMap = ids.stream().filter(dept -> dept.getId() > 20)
            .collect(Collectors.toMap(Dept::getId, dept -> dept));
    System.out.println("collectMap:" + collectMap);
}

結果如下:

collectList:[Dept{id=22}, Dept{id=23}]
collectSet:[Dept{id=23}, Dept{id=22}]
collectMap:{22=Dept{id=22}, 23=Dept{id=23}}

生成拼接字串

使用collect實現:

public void testCollectJoinStrings() {
    List<String> ids = Arrays.asList("205", "10", "308", "49", "627", "193", "111", "193");
    String joinResult = ids.stream().collect(Collectors.joining(","));
    System.out.println("拼接後:" + joinResult);
}

結果:

拼接後:205,10,308,49,627,193,111,193

資料批次數學運算

還有一種場景,實際使用的時候可能會比較少,就是使用collect生成數字資料的總和資訊,也可以瞭解下實現方式:

public void testNumberCalculate() {
    List<Integer> ids = Arrays.asList(10, 20, 30, 40, 50);
    // 計算平均值
    Double average = ids.stream().collect(Collectors.averagingInt(value -> value));
    System.out.println("平均值:" + average);
    // 資料統計資訊
    IntSummaryStatistics summary = ids.stream().collect(Collectors.summarizingInt(value -> value));
    System.out.println("資料統計資訊: " + summary);
}

上面的例子中,使用collect方法來對list中元素值進行數學運算,結果如下:

平均值:30.0
總和: IntSummaryStatistics{count=5, sum=150, min=10, average=30.000000, max=50}

總結

Stream相較於傳統的foreach的方式處理stream,到底有啥優勢?

根據前面的介紹,我們應該可以得出如下幾點答案:

  • 程式碼更簡潔、偏宣告式的編碼風格,更容易體現出程式碼的邏輯意圖
  • 邏輯間解耦,一個stream中間處理邏輯,無需關注上游與下游的內容,只需要按約定實現自身邏輯即可
  • 並行流場景效率會比迭代器逐個迴圈更高
  • 函式式介面,延遲執行的特性,中間管道操作不管有多少步驟都不會立即執行,只有遇到終止操作的時候才會開始執行,可以避免一些中間不必要的操作消耗

當然了,Stream也不全是優點,在有些方面也有其弊端:

  • 程式碼調測debug不便
  • 程式設計師從歷史寫法切換到Stream時,需要一定的適應時間

參考:
全面吃透JAVA Stream流操作,讓程式碼更加的優雅
Java8使用Stream流實現List列表的遍歷、統計、排序等
Java8 Stream 用法大全
Java 8中 Stream 流詳解

相關文章