首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 相關知識

Java 去除集合中重複物件完整方案:equals/hashCode 契約、Stream distinct、LinkedHashSet 保序去重

     編輯:Java 相關知識

前言

集合去重是高頻業務需求。核心在於**正確實現 `equals`/`hashCode` 契約**,配合 `Set` 特性或 Stream API 實現高效去重。本文系統闡述去重原理、5 大方案對比、效能測試與最佳實踐。

一、去重核心:equals/hashCode 契約

1.1 契約規則

  1. 若 `a.equals(b) == true`,則 `a.hashCode() == b.hashCode()` **必須成立**
  2. 若 `a.hashCode() == b.hashCode()`,**不要求** `a.equals(b) == true`(允許雜湊碰撞)
  3. `equals` 需滿足:自反性、對稱性、傳遞性、一致性、非空性

1.2 標準實現範例


public class Person {
    private String name;
    private int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    // Getter...

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Person person = (Person) o;
        return age == person.age && Objects.equals(name, person.name);
    }

    @Override
    public int hashCode() {
        return Objects.hash(name, age);
    }
}
  • **IDE 自動生成**最可靠
  • **所有參與 equals 的欄位必須參與 hashCode**
  • **可變欄位慎用於 equals/hashCode**

二、五大去重方案對比

2.1 HashSet 去重(無序、最高效)


List list = Arrays.asList(
    new Person("Alice", 20), new Person("Bob", 25),
    new Person("Alice", 20), new Person("Charlie", 30)
);

Set set = new HashSet<>(list); // 去重
List distinct = new ArrayList<>(set); // 轉回 List
  • 時間 O(n)、空間 O(n)
  • **不保持原順序**

2.2 LinkedHashSet 去重(保序、推薦通用)


List distinct = new ArrayList<>(new LinkedHashSet<>(list));
// 保持首次出現順序
  • 雙向鏈表維護插入順序
  • 效能僅略低於 HashSet,**生產環境首選**

2.3 Stream.distinct()(聲明式、保序)


List distinct = list.stream().distinct().collect(Collectors.toList());
// 內部用 LinkedHashSet 實現,保序
  • 語義清晰、鏈式操作
  • 可組合 `filter`、`sorted`、`map` 等

2.4 TreeSet 去重(排序去重)


// Person 需實現 Comparable 或提供 Comparator
List distinct = new ArrayList<>(new TreeSet<>(list)); // 按自然序/比較器排序
  • 紅黑樹 O(log n) 插入,總 O(n log n)
  • 需實現 `Comparable` 或提供 `Comparator`
  • **比較器必須與 equals 一致**:`compare(a,b)==0 <=> a.equals(b)`

2.5 手動迭代去重(教學用、可控邏輯)


public static  List distinctManual(List list) {
    List result = new ArrayList<>();
    for (T item : list) {
        if (!result.contains(item)) { // O(n²) 效能差
            result.add(item);
        }
    }
    return result;
}
  • 時間 O(n²):**大數據量嚴禁使用**
  • 優點:可自定義「重複」判斷邏輯(如只比較 ID)

三、效能基準測試(JMH)


@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MICROSECONDS)
@State(Scope.Thread)
public class DistinctBenchmark {
    @Param({"100", "1000", "10000", "100000"})
    int size;
    List list;

    @Setup
    public void setup() {
        list = new ArrayList<>(size);
        for (int i = 0; i < size; i++) {
            list.add(new Person("Name" + (i % (size / 10)), i % 100)); // 10% 重複率
        }
    }

    @Benchmark public List hashSet() { return new ArrayList<>(new HashSet<>(list)); }
    @Benchmark public List linkedHashSet() { return new ArrayList<>(new LinkedHashSet<>(list)); }
    @Benchmark public List stream() { return list.stream().distinct().collect(Collectors.toList()); }
    @Benchmark public List treeSet() { return new ArrayList<>(new TreeSet<>(list)); }

    /* 典型結果(微秒):
    size=100    hashSet: 15  linkedHashSet: 18  stream: 20  treeSet: 45
    size=1000   hashSet: 120 linkedHashSet: 140 stream: 160 treeSet: 450
    size=10000  hashSet: 1.2ms linkedHashSet: 1.4ms stream: 1.6ms treeSet: 5ms
    size=100000 hashSet: 12ms linkedHashSet: 14ms stream: 16ms treeSet: 55ms
    */
}

關鍵結論

  1. HashSet/LinkedHashSet/Stream 效能同量級,TreeSet 因排序慢 3-5 倍
  2. LinkedHashSet 保序開銷極小,**通用場景首選**
  3. Stream.distinct 內部即用 LinkedHashSet**,語義更清晰

四、進階場景與變體

4.1 按指定屬性去重(如只比 ID)


// 方案 1:Stream + 狀態收集器
List distinctById = list.stream()
    .collect(Collectors.collectingAndThen(
        Collectors.toCollection(() -> new TreeSet<>(Comparator.comparing(Person::getId))),
        ArrayList::new
    ));

// 方案 2:自定義 Predicate(線程不安全,單執行緒可用)
Set seenIds = ConcurrentHashMap.newKeySet();
List distinct = list.stream()
    .filter(p -> seenIds.add(p.getId()))
    .collect(Collectors.toList());

4.2 基本型別陣列去重


int[] arr = {1, 2, 3, 2, 4, 1, 5};
// 方案 1:排序後相鄰去重
Arrays.sort(arr);
int[] distinct = IntStream.of(arr).distinct().toArray(); // Stream 需裝箱

// 方案 2:Bitset(值域小,如 0-1000)
BitSet bitSet = new BitSet();
for (int v : arr) bitSet.set(v);
int[] distinct = new int[bitSet.cardinality()];
int idx = 0;
for (int i = bitSet.nextSetBit(0); i >= 0; i = bitSet.nextSetBit(i + 1)) {
    distinct[idx++] = i;
}

4.3 並發環境去重


// 併發安全 Set
Set concurrentSet = ConcurrentHashMap.newKeySet();
// 或
Set copyOnWriteSet = new CopyOnWriteArraySet<>(); // 讀多寫少

五、避坑指南

  1. 未覆寫 equals/hashCode:預設 `Object.equals` 比較引用,**無法去除內容相同的不同物件**
  2. equals 用 getter,hashCode 用欄位**:不一致導致 `HashSet` 失效
  3. 可變物件放入 Set 後修改欄位**:雜湊碼變化導致「找不到自己」,**嚴禁**
  4. TreeSet 比較器與 equals 不一致**:`compare(a,b)==0` 但 `!a.equals(b)` 違反 Set 契約
  5. 大量資料去重**:考慮資料庫 `DISTINCT`、Bloom Filter 預過濾、分批處理

總結

**正確實現 `equals`/`hashCode` 是去重前提**。通用場景**首選 `LinkedHashSet` 或 `Stream.distinct()`**,兼顧效能與保序。按屬性去重用 `TreeSet` + 比較器或 `filter` + `seenSet`。大數據量下游至資料庫層或流式處理框架。理解 `Set` 底層雜湊表機制、雜湊碰撞處理、紅黑樹排序特性,才能在複雜業務中游刃有餘。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved