Java Stream的distinct去重原理分析
一、distinct 的基礎用法與核心特性
distinct()
是 Stream API 中的有狀態(tài)中間操作,用于移除流中的重復元素,其底層依賴元素的hashCode()
和equals()
方法。用法示例:
List<Integer> numbers = Arrays.asList(1, 2, 2, 3, 4, 4); List<Integer> unique = numbers.stream() .distinct() .collect(Collectors.toList()); // [1, 2, 3, 4]
核心特性:
- 去重邏輯基于元素的唯一性標識,而非內存地址;
- 保持元素首次出現(xiàn)的順序;
- 屬于有狀態(tài)操作,處理過程中需維護已出現(xiàn)元素的集合。
二、distinct 的底層實現(xiàn)原理
1. 順序流中的去重實現(xiàn)
順序流中,distinct()
通過HashSet
存儲已處理元素,流程如下:
- 遍歷流中的每個元素;
- 對每個元素計算
hashCode()
,檢查HashSet
中是否存在相同哈希值的元素; - 若存在,進一步通過
equals()
比較內容,相同則過濾; - 若不存在,將元素添加到
HashSet
并保留在流中。
源碼關鍵片段(JDK 17):
// ReferencePipeline.java public final Stream<P_OUT> distinct() { return new DistinctOps<P_OUT, P_OUT>(this); } // DistinctOps.java @Override public void accept(P_OUT t) { if (set.add(t)) { // 調用HashSet的add方法,返回false表示重復 down.accept(t); } }
2. 并行流中的去重優(yōu)化
并行流中,distinct()
使用ConcurrentHashMap
或分段處理提升性能:
- 將流分割為多個子任務,每個子任務維護獨立的
HashSet
; - 子任務處理完成后,合并所有
HashSet
的結果; - 合并時使用
HashMap
去重,避免并發(fā)沖突。
并行處理示意圖:
+----------------+ +----------------+ +----------------+ | 子任務1: HashSet |---->| 子任務2: HashSet |---->| 合并階段: HashMap | | 存儲元素A,B,C | | 存儲元素B,D,E | | 最終結果A,B,C,D,E | +----------------+ +----------------+ +----------------+
三、去重邏輯的核心依賴:hashCode 與 equals
1. 自定義對象的去重規(guī)則
若需對自定義對象去重,必須正確重寫hashCode()
和equals()
:
class User { private String id; private String name; @Override public int hashCode() { return Objects.hash(id); // 僅用id計算哈希值 } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; User user = (User) o; return Objects.equals(id, user.id); // 僅比較id } // 其他方法省略 } // 使用示例 List<User> users = Arrays.asList( new User("1", "Alice"), new User("1", "Bob"), // id相同,會被去重 new User("2", "Charlie") ); List<User> uniqueUsers = users.stream() .distinct() .collect(Collectors.toList()); // 保留兩個用戶
2. 常見誤區(qū):僅重寫 equals 不重寫 hashCode
若只重寫equals
,會導致去重失效,因為HashSet
首先通過hashCode
判斷元素是否存在:
class ErrorUser { private String id; // 錯誤:未重寫hashCode @Override public boolean equals(Object o) { // 正確實現(xiàn)equals... } } // 使用distinct時,兩個id相同的ErrorUser可能因hashCode不同被視為不同元素
四、distinct 的性能影響與優(yōu)化策略
1. 性能損耗的主要原因
- 內存占用:需存儲所有已出現(xiàn)元素,大數(shù)據(jù)集可能導致 OOM;
- 哈希計算開銷:每個元素需計算
hashCode
并進行哈希表查找; - 并行流的合并開銷:多線程環(huán)境下的集合合并操作耗時。
2. 大數(shù)據(jù)集的去重優(yōu)化
- 預排序 + 相鄰去重:對有序流使用
distinct()
效率更高,因重復元素相鄰時哈希表查找次數(shù)減少
// 優(yōu)化前:無序流去重 List<Integer> randomData = getRandomNumbers(1000000); randomData.stream().distinct().count(); // 全量哈希表查找 // 優(yōu)化后:先排序再去重 randomData.stream() .sorted() .distinct() .count(); // 相鄰重復元素只需一次比較
- 使用 Primitive Stream 減少裝箱:
// 低效:對象流裝箱 Stream<Integer> boxedStream = data.stream().distinct(); // 高效:IntStream直接操作 IntStream primitiveStream = data.stream().mapToInt(Integer::intValue).distinct();
- 分塊處理大集合:避免一次性加載所有元素到內存
// 分塊去重示例 int chunkSize = 100000; List<Integer> result = new ArrayList<>(); for (int i = 0; i < data.size(); i += chunkSize) { int end = Math.min(i + chunkSize, data.size()); List<Integer> chunk = data.subList(i, end); result.addAll(chunk.stream().distinct().collect(Collectors.toList())); } // 最后再去重一次合并結果 List<Integer> finalResult = result.stream().distinct().collect(Collectors.toList());
3. 并行流去重的參數(shù)調優(yōu)
通過自定義Spliterator
控制分塊大小,減少合并開銷:
class EfficientSpliterator implements Spliterator<Integer> { private final List<Integer> list; private int index; private static final int CHUNK_SIZE = 10000; // 分塊大小 public EfficientSpliterator(List<Integer> list) { this.list = list; this.index = 0; } @Override public Spliterator<Integer> trySplit() { int size = list.size() - index; if (size < CHUNK_SIZE) return null; int splitPos = index + size / 2; Spliterator<Integer> spliterator = new EfficientSpliterator(list.subList(index, splitPos)); index = splitPos; return spliterator; } // 其他方法省略... } // 使用示例 List<Integer> data = ...; Stream<Integer> optimizedStream = StreamSupport.stream( new EfficientSpliterator(data), true); // 啟用并行
五、特殊場景的去重方案
1. 基于部分屬性的去重
若需根據(jù)對象的部分屬性去重(而非全部屬性),可結合map
和collect
:
class Product { private String id; private String name; private double price; // 構造器、getter省略 } // 按id去重 List<Product> uniqueProducts = products.stream() .collect(Collectors.collectingAndThen( Collectors.toMap(Product::getId, p -> p, (p1, p2) -> p1), map -> new ArrayList<>(map.values()) ));
2. 去重并保留最新元素
在日志等場景中,需按時間戳去重并保留最新記錄:
class LogEntry { private String message; private long timestamp; // 構造器、getter省略 } List<LogEntry> latestLogs = logs.stream() .collect(Collectors.toMap( LogEntry::getMessage, entry -> entry, (oldEntry, newEntry) -> newEntry.getTimestamp() > oldEntry.getTimestamp() ? newEntry : oldEntry )) .values() .stream() .collect(Collectors.toList());
3. 模糊去重(非精確匹配)
如需基于相似度去重(如字符串編輯距離),需自定義去重邏輯:
List<String> fuzzyUnique = strings.stream() .filter(s -> !strings.stream() .anyMatch(t -> s != t && levenshteinDistance(s, t) < 2)) .collect(Collectors.toList());
六、性能對比:distinct 與其他去重方式
去重方式 | 大數(shù)據(jù)集性能 | 內存占用 | 實現(xiàn)復雜度 | 適用場景 |
---|---|---|---|---|
Stream.distinct() | 中 | 高(存儲所有元素) | 低 | 通用去重 |
先排序 + 相鄰去重 | 高 | 中 | 中 | 有序數(shù)據(jù)去重 |
HashSet 直接去重 | 高 | 高 | 低 | 簡單集合去重 |
分塊去重 | 高 | 低 | 高 | 超大數(shù)據(jù)集去重 |
總結
distinct()
作為 Stream API 中的基礎操作,其核心去重邏輯依賴于hashCode()
和equals()
的正確實現(xiàn),而性能優(yōu)化的關鍵在于:
- 數(shù)據(jù)有序性利用:先排序再去重可減少哈希表查找次數(shù);
- 內存占用控制:對大數(shù)據(jù)集采用分塊處理,避免一次性存儲所有元素;
- 基礎類型優(yōu)化:使用
IntStream
等避免裝箱損耗; - 并行處理調優(yōu):通過自定義
Spliterator
控制分塊大小,減少合并開銷。
理解distinct()
的底層實現(xiàn)原理,不僅能避免自定義對象去重時的常見錯誤,更能在處理大規(guī)模數(shù)據(jù)時選擇合適的優(yōu)化策略。記?。喝ブ夭僮鞯谋举|是空間與時間的權衡,根據(jù)具體業(yè)務場景(數(shù)據(jù)規(guī)模、有序性、精確性要求)選擇最優(yōu)方案,才能實現(xiàn)性能與功能的平衡。
以上就是Java Stream的distinct去重原理分析的詳細內容,更多關于Java Stream distinct去重的資料請關注腳本之家其它相關文章!
相關文章
詳解Java是如何通過接口來創(chuàng)建代理并進行http請求
今天給大家?guī)淼闹R是關于Java的,文章圍繞Java是如何通過接口來創(chuàng)建代理并進行http請求展開,文中有非常詳細的介紹及代碼示例,需要的朋友可以參考下2021-06-06