Java Stream的distinct去重原理分析
一、distinct 的基礎(chǔ)用法與核心特性
distinct()是 Stream API 中的有狀態(tài)中間操作,用于移除流中的重復(fù)元素,其底層依賴元素的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]核心特性:
- 去重邏輯基于元素的唯一性標(biāo)識(shí),而非內(nèi)存地址;
- 保持元素首次出現(xiàn)的順序;
- 屬于有狀態(tài)操作,處理過程中需維護(hù)已出現(xiàn)元素的集合。
二、distinct 的底層實(shí)現(xiàn)原理
1. 順序流中的去重實(shí)現(xiàn)
順序流中,distinct()通過HashSet存儲(chǔ)已處理元素,流程如下:
- 遍歷流中的每個(gè)元素;
- 對(duì)每個(gè)元素計(jì)算
hashCode(),檢查HashSet中是否存在相同哈希值的元素; - 若存在,進(jìn)一步通過
equals()比較內(nèi)容,相同則過濾; - 若不存在,將元素添加到
HashSet并保留在流中。
源碼關(guān)鍵片段(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)) { // 調(diào)用HashSet的add方法,返回false表示重復(fù)
down.accept(t);
}
}2. 并行流中的去重優(yōu)化
并行流中,distinct()使用ConcurrentHashMap或分段處理提升性能:
- 將流分割為多個(gè)子任務(wù),每個(gè)子任務(wù)維護(hù)獨(dú)立的
HashSet; - 子任務(wù)處理完成后,合并所有
HashSet的結(jié)果; - 合并時(shí)使用
HashMap去重,避免并發(fā)沖突。
并行處理示意圖:
+----------------+ +----------------+ +----------------+ | 子任務(wù)1: HashSet |---->| 子任務(wù)2: HashSet |---->| 合并階段: HashMap | | 存儲(chǔ)元素A,B,C | | 存儲(chǔ)元素B,D,E | | 最終結(jié)果A,B,C,D,E | +----------------+ +----------------+ +----------------+
三、去重邏輯的核心依賴:hashCode 與 equals
1. 自定義對(duì)象的去重規(guī)則
若需對(duì)自定義對(duì)象去重,必須正確重寫hashCode()和equals():
class User {
private String id;
private String name;
@Override
public int hashCode() {
return Objects.hash(id); // 僅用id計(jì)算哈希值
}
@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相同,會(huì)被去重
new User("2", "Charlie")
);
List<User> uniqueUsers = users.stream()
.distinct()
.collect(Collectors.toList()); // 保留兩個(gè)用戶2. 常見誤區(qū):僅重寫 equals 不重寫 hashCode
若只重寫equals,會(huì)導(dǎo)致去重失效,因?yàn)?code>HashSet首先通過hashCode判斷元素是否存在:
class ErrorUser {
private String id;
// 錯(cuò)誤:未重寫hashCode
@Override
public boolean equals(Object o) {
// 正確實(shí)現(xiàn)equals...
}
}
// 使用distinct時(shí),兩個(gè)id相同的ErrorUser可能因hashCode不同被視為不同元素四、distinct 的性能影響與優(yōu)化策略
1. 性能損耗的主要原因
- 內(nèi)存占用:需存儲(chǔ)所有已出現(xiàn)元素,大數(shù)據(jù)集可能導(dǎo)致 OOM;
- 哈希計(jì)算開銷:每個(gè)元素需計(jì)算
hashCode并進(jìn)行哈希表查找; - 并行流的合并開銷:多線程環(huán)境下的集合合并操作耗時(shí)。
2. 大數(shù)據(jù)集的去重優(yōu)化
- 預(yù)排序 + 相鄰去重:對(duì)有序流使用
distinct()效率更高,因重復(fù)元素相鄰時(shí)哈希表查找次數(shù)減少
// 優(yōu)化前:無序流去重
List<Integer> randomData = getRandomNumbers(1000000);
randomData.stream().distinct().count(); // 全量哈希表查找
// 優(yōu)化后:先排序再去重
randomData.stream()
.sorted()
.distinct()
.count(); // 相鄰重復(fù)元素只需一次比較- 使用 Primitive Stream 減少裝箱:
// 低效:對(duì)象流裝箱 Stream<Integer> boxedStream = data.stream().distinct(); // 高效:IntStream直接操作 IntStream primitiveStream = data.stream().mapToInt(Integer::intValue).distinct();
- 分塊處理大集合:避免一次性加載所有元素到內(nèi)存
// 分塊去重示例
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()));
}
// 最后再去重一次合并結(jié)果
List<Integer> finalResult = result.stream().distinct().collect(Collectors.toList());3. 并行流去重的參數(shù)調(diào)優(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ù)對(duì)象的部分屬性去重(而非全部屬性),可結(jié)合map和collect:
class Product {
private String id;
private String name;
private double price;
// 構(gòu)造器、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. 去重并保留最新元素
在日志等場景中,需按時(shí)間戳去重并保留最新記錄:
class LogEntry {
private String message;
private long timestamp;
// 構(gòu)造器、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());六、性能對(duì)比:distinct 與其他去重方式
| 去重方式 | 大數(shù)據(jù)集性能 | 內(nèi)存占用 | 實(shí)現(xiàn)復(fù)雜度 | 適用場景 |
|---|---|---|---|---|
| Stream.distinct() | 中 | 高(存儲(chǔ)所有元素) | 低 | 通用去重 |
| 先排序 + 相鄰去重 | 高 | 中 | 中 | 有序數(shù)據(jù)去重 |
| HashSet 直接去重 | 高 | 高 | 低 | 簡單集合去重 |
| 分塊去重 | 高 | 低 | 高 | 超大數(shù)據(jù)集去重 |
總結(jié)
distinct()作為 Stream API 中的基礎(chǔ)操作,其核心去重邏輯依賴于hashCode()和equals()的正確實(shí)現(xiàn),而性能優(yōu)化的關(guān)鍵在于:
- 數(shù)據(jù)有序性利用:先排序再去重可減少哈希表查找次數(shù);
- 內(nèi)存占用控制:對(duì)大數(shù)據(jù)集采用分塊處理,避免一次性存儲(chǔ)所有元素;
- 基礎(chǔ)類型優(yōu)化:使用
IntStream等避免裝箱損耗; - 并行處理調(diào)優(yōu):通過自定義
Spliterator控制分塊大小,減少合并開銷。
理解distinct()的底層實(shí)現(xiàn)原理,不僅能避免自定義對(duì)象去重時(shí)的常見錯(cuò)誤,更能在處理大規(guī)模數(shù)據(jù)時(shí)選擇合適的優(yōu)化策略。記住:去重操作的本質(zhì)是空間與時(shí)間的權(quán)衡,根據(jù)具體業(yè)務(wù)場景(數(shù)據(jù)規(guī)模、有序性、精確性要求)選擇最優(yōu)方案,才能實(shí)現(xiàn)性能與功能的平衡。
以上就是Java Stream的distinct去重原理分析的詳細(xì)內(nèi)容,更多關(guān)于Java Stream distinct去重的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
詳解Java是如何通過接口來創(chuàng)建代理并進(jìn)行http請求
今天給大家?guī)淼闹R(shí)是關(guān)于Java的,文章圍繞Java是如何通過接口來創(chuàng)建代理并進(jìn)行http請求展開,文中有非常詳細(xì)的介紹及代碼示例,需要的朋友可以參考下2021-06-06
Mybatis插件之自動(dòng)生成不使用默認(rèn)的駝峰式操作
這篇文章主要介紹了Mybatis插件之自動(dòng)生成不使用默認(rèn)的駝峰式操作,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2020-11-11
SWT(JFace) 體驗(yàn)之FontRegistry
測試代碼如下:2009-06-06
java isInterrupted()判斷線程的實(shí)例講解
在本篇內(nèi)容里小編給大家分享的是一篇關(guān)于java isInterrupted()判斷線程的實(shí)例講解內(nèi)容,有興趣的朋友們可以學(xué)習(xí)下。2021-05-05

