- 前言
- 一、集合的比較
- 1.1需要得到一個新的流
- 1.2只需要一個簡單 boolean 結果
- 二、簡單集合的對比
- 2.1整型元素集合
- 2.2字串元素集合
- 2.3其它比較
- 三、Stream 基礎回顧
- 3.1基本概念
- 3.2 Stream 操作的三個步驟
- 3.2.1建立 Stream
- 3.2.2中間操作
- 3.2.3終止操作
- 四、文章小結
前言
在之前的開發中,遇到了這樣的需求:記錄某個更新操作之前的資料作為日誌內容,之後可以供管理員在頁面上檢視該日誌。
思路:
- 更新介面拿入參與現在資料庫該條資料逐一對比,將不同的部分取出;
- 在更新操作前取出現在資料庫的該條資料,更新操作後再取出同一條資料,比較兩者的異同。
經過短暫對比後,我選擇方案2,理由如下:
- 前端入參未經過後端真實性校驗,即萬一進來的不是同一條資料呢?這樣是不可靠的。
- 後端先拿引數去資料庫找,如果有這條資料,那麼拿出來做對比可以保證更新的是同一條資料。
要點:
- 從資料庫裡拿出來的一條資料其實是個實體類物件,那是否可以兩個物件逐一比較屬性值是否相等呢?這個不現實,因為引用型別的物件在記憶體中的地址肯定不同,所以物件 .equals() 的結果永遠是 false;
- 既然物件不能直接比較,那麼就將其先轉換為一個集合後再進行 Stream 操作;
- 這裡需要比較的兩個集合的元素屬性名相同,但是值不一定相同;
一、集合的比較
具體情況可以分為: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.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 所示:
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
方法 | 描述 |
---|---|
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 |
返回流中第一個元素並放置到 Optional 容器中 |
Optional |
返回流中任意一個元素並放置到 Optional 容器中 |
long count(); | 返回流中元素的總個數 |
Optional |
經比較器按順序比較後,返回流中最大值 |
Optional |
經比較器按順序比較後,返回流中最小值 |
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“按鈕,可以追蹤當前流中的鏈式變化。
那麼今天的分享到這裡就結束了,如有不足和錯誤,還請大家指正。或者你有其它想說的,也歡迎大家在評論區交流!
參考文件:
- https://blog.csdn.net/k8080880/article/details/134153041
- https://blog.csdn.net/weixin_52793468/article/details/132901759
- https://www.cnblogs.com/vic-tory/p/17811250.html