【進階篇】使用 Stream 流對比兩個集合的常用操作分享

来源:https://www.cnblogs.com/CodeBlogMan/p/18156081
-Advertisement-
Play Games

Stream API 是 Java 8 中最為重要的更新之一,是處理集合的關鍵抽象概念,也是每個 Java 後端開發人員都必須無條件掌握的內容。 在之前的開發中,遇到了這樣的需求:記錄某個更新操作之前的數據作為日誌內容,之後可以供管理員在頁面上查看該日誌。 ...


目錄

前言

在之前的開發中,遇到了這樣的需求:記錄某個更新操作之前的數據作為日誌內容,之後可以供管理員在頁面上查看該日誌。

思路:

  1. 更新介面拿入參與現在資料庫該條數據逐一對比,將不同的部分取出;
  2. 在更新操作前取出現在資料庫的該條數據,更新操作後再取出同一條數據,比較兩者的異同。

經過短暫對比後,我選擇方案2,理由如下:

  • 前端入參未經過後端真實性校驗,即萬一進來的不是同一條數據呢?這樣是不可靠的。
  • 後端先拿參數去資料庫找,如果有這條數據,那麼拿出來做對比可以保證更新的是同一條數據。

要點:

  1. 從資料庫里拿出來的一條數據其實是個實體類對象,那是否可以兩個對象逐一比較屬性值是否相等呢?這個不現實,因為引用類型的對象在記憶體中的地址肯定不同,所以對象 .equals() 的結果永遠是 false;
  2. 既然對象不能直接比較,那麼就將其先轉換為一個集合後再進行 Stream 操作;
  3. 這裡需要比較的兩個集合的元素屬性名相同,但是值不一定相同;

一、集合的比較

具體情況可以分為:1、是否需要得到一個新的流?2、是否只需要一個簡單 boolean 結果?

我開發需求是要得到具體哪些數據不一樣,所以選擇返回一個新的流,只是得到一個 boolean 來判斷是否相同是不夠的。

1.1需要得到一個新的流

  • 如果是得到一個新的流,那麼推薦使用.filter() + .collect()

        @Test
        public void testFilter(){
            //第一個數組
            List<ListData> list1 = new ArrayList<>();
            list1.add(new ListData("測測名字11",11,"email@11"));
            list1.add(new ListData("測測名字22",22,"email@22"));
            list1.add(new ListData("測測名字33",33,"email@33"));
            log.info("第一個數組為:{}", list1);
            //第二個數組
            List<ListData> list2 = new ArrayList<>();
            list2.add(new ListData("測測名字111",111,"email@11"));
            list2.add(new ListData("測測名字22",22,"email@22"));
            list2.add(new ListData("測測名字33",33,"email@33"));
            log.info("第二個數組為:{}", list2);
            //返回一個新的結果數組
            List<ListData> resultList = list1.stream()
                //最外層的filter里是條件,這個條件需要返回一個boolean:符合條件返回true,不符合條件返回false
                .filter(p1 -> list2.stream()
                        //這個filter也是條件:判斷兩個數組裡名字和年齡是否都相等,符合條件返回true,不符合條件返回false
                        .filter(p2 -> p2.getName().equals(p1.getName()) && p2.getAge().equals(p1.getAge()))
                        //如有內容則返迴流中的第一條記錄,其它情況都返回空
                        .findFirst().orElse(null)
                        //這個是最外層的filter的斷言
                        == null)
                //將上一步流處理的的結果,收集成一個新的集合
                .collect(Collectors.toList());
            log.info("經過 Stream 流處理後輸出的結果數組為: {}", resultList);
        }
    

    結合.filter() + noneMatch() 其實也與上面的語句效果相同:

           List<ListData> resultList = list1.stream()
                   .filter(p1 -> list2.stream()
                            //這個 noneMatch 也是條件:判斷兩個數組裡名字和年齡是否都相等,符合條件返回true,不符合條件返回false
                            .noneMatch(p2 -> p2.getName().equals(p1.getName()) && p2.getAge().equals(p1.getAge())))
                    .collect(Collectors.toList());
           log.info("經過 Stream 流處理後輸出的結果數組為: {}", resultList);
    

    結合 filter() + contains() 方法( 其中 contains() 方法的使用詳見 1.2 小節的註意事項),與以上的效果也一樣:

          List<ListData> resultList = list1.stream().filter(p1 -> !list2.contains(p1)).collect(Collectors.toList());
          log.info("經過 Stream 流處理後輸出的結果數組為: {}", resultList);
    

    下麵是以上代碼的運行結果如圖 1 所示:

圖1

1.2只需要一個簡單 boolean 結果

  • 如果只需要一個簡單的 boolean 結果,那麼推薦使用.anyMatch() 或者 allMatch()

            //返回一個boolean結果
            boolean flag = list1.stream()
                    //只要流中任意一個元素符合條件則返回true,否則返回false
                    .anyMatch(p1 -> list2.stream()
                            //如果流中全部元素都符合條件,就返回true,否則返回false;當流為空時總是返回true
                            .allMatch(p2 -> p2.getName().equals(p1.getName()) && p2.getAge().equals(p1.getAge())));
            log.info("經過 Stream 流對比是否相等: {}", flag);
    

    下麵是以上代碼的運行結果如圖 2 所示:

    圖2
  • 除了 Stream 流之外,還可以使用 JDK 自帶的.contains() 相關方法來判斷

    //List 集合介面自帶的方法 
    boolean isEqual = list1.containsAll(list2) && list2.containsAll(list1);
    
    //與上述方法效果一致
    boolean isEqual = list1.stream().anyMatch(p1 -> list2.contains(p1));
    //下麵的是上述語句的 lambda 表達式寫法
    //boolean isEqual = list1.stream().anyMatch(list2::contains);
    

    註意事項:.contains() 相關方法底層是迭代器 Iterator 以及 .equals() 方法,需要為 List 集合包含的泛型 中重寫.equals() 方法才能使用,舉例如下所示:

    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    public class ListData {
        private String name;
        private Integer age;
        private String email;
        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;
            ListData listData = (ListData) o;
            return Objects.equals(name, listData.name) && Objects.equals(age, listData.age) && Objects.equals(email, listData.email);
        }
    }
    

    下麵是以上代碼的運行結果如圖 3 所示:

    圖3
  • 理論上可以用 for 迴圈或者迭代器來做,效果與使用 .containsAll() 方法差不多,但是自己手寫的話可能會比較複雜,數據量稍大些的話效率較低,一般不考慮採用,這裡我就不演示了。


二、簡單集合的對比

上述的集合都是泛型為自定義引用類型的集合,下麵分享一些簡單集合,如整形、字元串類型集合的 Stream 流對比操作。

2.1整型元素集合

        List<Integer> list1 = Arrays.asList(1, 6,);
        List<Integer> list2 = Arrays.asList(3, 2, 1);
        //Java 本身提供的 Integer 類已經實現了 Comparable 介面,可以直接.sort() 比較
        boolean isEqual = list1.stream().sorted().collect(Collectors.toList())
            .equals(list2.stream().sorted().collect(Collectors.toList()));
        log.info("是否相等:{}", isEqual);

2.2字元串元素集合

        // 先排序然後轉成 String 逗號分隔,joining()拼接
        List<String> list3 = Arrays.asList("語文","數學","英語");
        List<String> list4 = Arrays.asList("數學","英語","語文");
        //Java 本身提供的 String 類也已經實現了 Comparable 介面
        boolean flag = list3.stream().sorted().collect(Collectors.toList())
            .equals(list4.stream().sorted().collect(Collectors.toList()));
        log.info("是否相等:{}", flag);

下麵是簡單集合比較的運行結果,如圖 4 所示:

圖4

2.3其它比較

不知道大家有沒有發現,上述簡單類型的類可以直接比較,而自己寫的類就不能,會報”cannot be cast to java.lang.Comparable“。

舉個例子,對於自定義的引用類型 ListData , Java 不知道應該怎樣為 ListData 的對象排序,是應該按名字排序? 還是按年齡來排序?

註意:.sort() 方法底層實現需要依賴 Comparator 介面,那麼這個引用類型 ListData 類要自己手動去實現 Comparator() 介面並重寫 compare() 方法才能這樣做比較。

        List<ListData> list1 = new ArrayList<>();
        list1.add(new ListData("泛型為引用類型", 666, "abc"));
        List<ListData> list2 = new ArrayList<>();
        list2.add(new ListData("泛型為引用類型", 888, "def"));
        //這裡想要收集成為集合進行比較,需要先根據特定的元素排序(年齡),然後再按順序比較
        boolean flag = list1.stream().sorted(Comparator.comparing(ListData::getAge)).collect(Collectors.toList())
                .equals(list2.stream().sorted(Comparator.comparing(ListData::getAge)).collect(Collectors.toList()));
        log.info("是否相等: {}", flag);

三、Stream 基礎回顧

Stream API 是 Java 8 中最為重要的更新之一,是處理集合的關鍵抽象概念,也是每個 Java 後端開發人員都必須無條件掌握的內容。

Stream 和 Collection 集合的主要區別:Collection 是記憶體數據結構,重在數據的存儲;而 Stream 是集合的操作計算,重在一系列的流式操作。

3.1基本概念

  • Stream 不會自己存儲元素,會返回一個持有結果的新的流;
  • Stream 操作是延遲執行的,即一旦執行終止操作,就執行中間操作鏈,並產生結果;
  • Stream 一旦執行了終止操作,那麼就不能再執行中間操作或者其它終止操作。

3.2 Stream 操作的三個步驟

3.2.1創建 Stream

一個數據源(如:集合、數組)來獲取一個流,具體有 3 種方式來創建:

  • 通過集合直接創建(最常用)

    //Java8 中的 Collection 介面被擴展,提供了兩個獲取流的方法:
    //返回一個順序流
    default Stream<E> stream(){}
    //返回一個並行流
    default Stream<E> parallelStream{}
    
  • Arrays 也可以獲取數組流

    //返回一個流
    public static <T> Stream<T> stream(T[] array){}
    
  • 調用 Stream 類靜態方法 of() 來創建流

    public static<T> Stream<T> of(T... values){}
    
3.2.2中間操作

每次處理都會返回一個持有結果的新 Stream,即中間操作的方法返回值仍然是 Stream 類型的對象。因此中間操作可以是鏈式的,可對數據源的數據進行 n 次處理,但是在終止操作前,並不會真正執行;

中間操作可謂是最重要也最常使用的操作,具體分為3種:篩選與切片、映射、排序,如以下表格所示:

  • 篩選與切片

    方法 描 述
    Stream filter(Predicate<? super T> predicate); 篩選,接收 Predicate 的條件,從流中排除某些元素,返回一個符合該條件的流
    Stream limit(long maxSize); 截斷,使其元素的數量不超過給定數量
    Stream skip(long n); 跳過,返回一個扔掉了前 n 個元素的流,若流中元素不足 n 個則返回一個空流,可與 limit() 形成互補
    Stream distinct(); 去重,利用流所生成元素的 hashCode() 和 equals() 去除流中的重覆元素
  • 映射

    這裡只介紹常見的映射方法,flatMap() 的系列方法並不常用。

    方法 描述
    Stream map(Function<? super T, ? extends R> mapper); 接收一個函數作為參數,該函數會被應用到每個元素上,並將其映射成一個新的元素。
    LongStream mapToLong(ToLongFunction<? super T> mapper); 接收一個函數作為參數,該函數會被應用到每個元素上,產生一個新的 Long 類型的Stream 流。
  • 排序

    方法 描述
    Stream sorted(); 產生一個新流,其中按自然順序(如Integer)排序
    Stream sorted(Comparator<? super T> comparator); 產生一個新流,其中按比較器指定的順序排序
3.2.3終止操作

終止操作的方法返回值類型不再是 Stream,而可以是任何不為流的值,如List、Integer 甚至是 void ,因此一旦執行終止操作就會結束整個 Stream操作且不能再次使用。終止操作也很常見,下麵就不做具體的分類,都寫在一起了,按需使用即可:

方法 描述
boolean anyMatch(Predicate<? super T> predicate); 檢查是否所有元素都符合條件,符合就返回 true,不符合則返回 false
boolean allMatch(Predicate<? super T> predicate); 檢查是否至少有一個元素符合條件,有則返回 true,無則返回false
boolean noneMatch(Predicate<? super T> predicate); 檢查是否所有元素都不匹配條件,都不符合則返回 true,其它情況返回false
Optional findFirst(); 返迴流中第一個元素並放置到 Optional 容器中
Optional findAny(); 返迴流中任意一個元素並放置到 Optional 容器中
long count(); 返迴流中元素的總個數
Optional max(Comparator<? super T> comparator); 經比較器按順序比較後,返迴流中最大值
Optional min(Comparator<? super T> comparator); 經比較器按順序比較後,返迴流中最小值
void forEach(Consumer<? super T> action); 內部迭代,如果要對集合迭代可以直接使用.foreach(),不必經過 Stream
<R, A> R collect(Collector<? super T, A, R> collector); 將流轉換為其他形式,如:將 Stream 中元素收集成.toList()、.toSet() 等

這裡有個特殊的方法,.groupingBy() 不屬於 Stream 而是屬於 Collectors:

方法 返回類型 描述
.stream().collect(Collectors.groupingBy()); public static <T, K> Collector<T, ?, Map<K, List>> 根據流中的某屬性值對流進行分組,屬性為 K,結果為指定的泛型,如 List

四、文章小結

文章到這裡就結束了,關於 Stream 流 API 是日常開發中經常會遇到的,熟練運用可以提高我們的開發效率,讓我們寫出簡潔易懂的代碼,我們作為後端開發必須重視起來。總有人說它的調試 debug 是個缺點,不妨試試”Trace Current Stream Chain“按鈕,可以追蹤當前流中的鏈式變化。

那麼今天的分享到這裡就結束了,如有不足和錯誤,還請大家指正。或者你有其它想說的,也歡迎大家在評論區交流!

參考文檔:


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 目的:求多個集合之前的並集,例如:現有四個集合C1 = {11, 22, 13, 14}、C2 = {11, 32, 23, 14, 35}、C3 = {11, 22, 38}、C4 = {11, 22, 33, 14, 55, 66},則它們之間的並集應該為: C1 & C2 & C3 = {11 ...
  • 1.排序方式 假設有一個序列,數據為:['n1', 'n2', 'n10', 'n11', 'n21', 'n3', 'n13', 'n20', 'n23'], 排序後需要達到這個效果:['n1', 'n2', 'n3', 'n10', 'n11', 'n13', 'n20', 'n21', 'n2 ...
  • 介紹 在學習了sylar的C++高性能分散式伺服器框架後,想把自己在學習過程中的感想記錄下來。當然主要原因還是sylar的B站視頻過於難以理解了,也是想加強一下自己對這個框架的理解。很多內容也是借鑒了其他大佬的博文,比如找人找不到北,zhongluqiang 日誌模塊概述 日誌模塊的目的: 用於格式 ...
  • 本文介紹JupyterLab中菜單欄按鈕無法點擊、快捷鍵無法執行問題的解決辦法。 近期打開JupyterLab後,發現其中菜單欄按鈕無法點擊,快捷鍵也均無法執行。如圖,紅框內的按鈕點擊均無任何反應。 為解決這一問題,首先嘗試關閉VPN、瀏覽器代理設置等,均不奏效。隨後,在搜索時看到Stack Ove ...
  • ​AV1是一種新興的免費視頻編碼標準,它由開放媒體聯盟(Alliance for Open Media,簡稱AOM)於2018年制定,融合了Google VP10、Mozilla Daala以及Cisco Thor三款開源項目的成果。據說在實際測試中,AV1標準比H.265(HEVC)的壓縮率提升了 ...
  • 最近網上衝浪的時候看到有人分享了自己最近一次性能優化的經驗。我向來對性能是比較敏感的,所以就點進去看了。 然而我越看越覺得蹊蹺,但本著“性能問題和性能優化要靠性能測試做依據”,我不能憑空懷疑別人吧,所以我做了完整的測試並寫下了這篇文章。 可疑的優化方案 分享者遇到的問題很簡單:他發現程式中超過一半的 ...
  • 本文介紹了註意力機制的基本原理,並使用 Python 和 TensorFlow/Keras 實現了一個簡單的註意力機制模型應用於文本分類任務。 ...
  • bolo-solo —— Bolo菠蘿博客,一個基於 Java 實現的博客系統,簡單易部署,具有精緻的主題,專為程式員設計。 ...
一周排行
    -Advertisement-
    Play Games
  • 前言 微服務架構已經成為搭建高效、可擴展系統的關鍵技術之一,然而,現有許多微服務框架往往過於複雜,使得我們普通開發者難以快速上手並體驗到微服務帶了的便利。為瞭解決這一問題,於是作者精心打造了一款最接地氣的 .NET 微服務框架,幫助我們輕鬆構建和管理微服務應用。 本框架不僅支持 Consul 服務註 ...
  • 先看一下效果吧: 如果不會寫動畫或者懶得寫動畫,就直接交給Blend來做吧; 其實Blend操作起來很簡單,有點類似於在操作PS,我們只需要設置關鍵幀,滑鼠點來點去就可以了,Blend會自動幫我們生成我們想要的動畫效果. 第一步:要創建一個空的WPF項目 第二步:右鍵我們的項目,在最下方有一個,在B ...
  • Prism:框架介紹與安裝 什麼是Prism? Prism是一個用於在 WPF、Xamarin Form、Uno 平臺和 WinUI 中構建鬆散耦合、可維護和可測試的 XAML 應用程式框架 Github https://github.com/PrismLibrary/Prism NuGet htt ...
  • 在WPF中,屏幕上的所有內容,都是通過畫筆(Brush)畫上去的。如按鈕的背景色,邊框,文本框的前景和形狀填充。藉助畫筆,可以繪製頁面上的所有UI對象。不同畫筆具有不同類型的輸出( 如:某些畫筆使用純色繪製區域,其他畫筆使用漸變、圖案、圖像或繪圖)。 ...
  • 前言 嗨,大家好!推薦一個基於 .NET 8 的高併發微服務電商系統,涵蓋了商品、訂單、會員、服務、財務等50多種實用功能。 項目不僅使用了 .NET 8 的最新特性,還集成了AutoFac、DotLiquid、HangFire、Nlog、Jwt、LayUIAdmin、SqlSugar、MySQL、 ...
  • 本文主要介紹攝像頭(相機)如何採集數據,用於類似攝像頭本地顯示軟體,以及流媒體數據傳輸場景如傳屏、視訊會議等。 攝像頭採集有多種方案,如AForge.NET、WPFMediaKit、OpenCvSharp、EmguCv、DirectShow.NET、MediaCaptre(UWP),網上一些文章以及 ...
  • 前言 Seal-Report 是一款.NET 開源報表工具,擁有 1.4K Star。它提供了一個完整的框架,使用 C# 編寫,最新的版本採用的是 .NET 8.0 。 它能夠高效地從各種資料庫或 NoSQL 數據源生成日常報表,並支持執行複雜的報表任務。 其簡單易用的安裝過程和直觀的設計界面,我們 ...
  • 背景需求: 系統需要對接到XXX官方的API,但因此官方對接以及管理都十分嚴格。而本人部門的系統中包含諸多子系統,系統間為了穩定,程式間多數固定Token+特殊驗證進行調用,且後期還要提供給其他兄弟部門系統共同調用。 原則上:每套系統都必須單獨接入到官方,但官方的接入複雜,還要官方指定機構認證的證書 ...
  • 本文介紹下電腦設備關機的情況下如何通過網路喚醒設備,之前電源S狀態 電腦Power電源狀態- 唐宋元明清2188 - 博客園 (cnblogs.com) 有介紹過遠程喚醒設備,後面這倆天瞭解多了點所以單獨加個隨筆 設備關機的情況下,使用網路喚醒的前提條件: 1. 被喚醒設備需要支持這WakeOnL ...
  • 前言 大家好,推薦一個.NET 8.0 為核心,結合前端 Vue 框架,實現了前後端完全分離的設計理念。它不僅提供了強大的基礎功能支持,如許可權管理、代碼生成器等,還通過採用主流技術和最佳實踐,顯著降低了開發難度,加快了項目交付速度。 如果你需要一個高效的開發解決方案,本框架能幫助大家輕鬆應對挑戰,實 ...