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

Java 去重大揭秘:從 List 到 Set,再到自定義對象 equals/hashCode 契約深度解析

     編輯:Java 相關知識

前言

「數組/集合去重」是高頻面試題與日常開發需求。許多開發者只知 new HashSet<>(list) 一把梭,卻在自定義對象去重時翻車。本文層層遞進:從基礎 API、原理機制、自定義對象陷阱,到 equals/hashCode 契約核心,構建完整去重知識體系。

一、基礎去重方案對比

1. 暴力 O(n²) —— 只有教學價值


// 重寫 ArrayList.add,每次遍歷判斷
List<String> list = new ArrayList<>() {
    @Override public boolean add(String e) {
        for (String s : this) if (s.equals(e)) return false;
        return super.add(e);
    }
};

❌ 10 萬數據新增一個需 10 萬次 equals,性能災難。

2. Set 特性去重 —— 標準做法


public static void removeDuplicate(List<Person> list) {
    Set<Person> set = new HashSet<>(list); // 利用 Set 無序、唯一特性
    list.clear();
    list.addAll(set); // 還原到 List(順序會打亂)
}

✅ O(n) 平均時間,但順序不保留。若需保序,用 LinkedHashSet。

二、為什麼自定義對象去重失效?

測試代碼:


Person[] data = { new Person(0,"a"), new Person(1,"b"), new Person(0,"a") };
List<Person> list = Arrays.asList(data);
removeDuplicate(list); // 期望去重,實際仍有 3 個元素

根因:HashSet 依賴 HashMap 的 Key 唯一性判斷

  1. HashSet.add(e) → HashMap.put(e, PRESENT)。
  2. HashMap.put 判斷 Key 重複邏輯:

// 簡化版 HashMap.put 核心邏輯
int hash = secondaryHash(key.hashCode());
int index = hash & (table.length - 1);
for (Entry e = table[index]; e != null; e = e.next) {
    if (e.hash == hash && key.equals(e.key)) { // 關鍵:hash相等 且 equals為true
        // 視為重複 Key,覆蓋 value
        return oldValue;
    }
}
// 插入新節點

結論:兩個對象被視為「相同」需要滿足雙重條件:

  1. hashCode() 相等
  2. equals() 返回 true

三、正確實現 equals/hashCode 契約

錯誤示範:未重寫,使用 Object 默認實現

  • equals 比較引用地址(==)。
  • hashCode 基於對象頭地址生成。
  • 結果:內容相同的兩個 new Person(0,"a") 被視為不同。

正確實現


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

    // 構造器、getter/setter、toString 省略

    @Override
    public boolean equals(Object obj) {
        if (this == obj) return true;
        if (!(obj instanceof Person)) return false;
        Person other = (Person) obj;
        return id == other.id && Objects.equals(name, other.name);
    }

    @Override
    public int hashCode() {
        return Objects.hash(id, name); // JDK 7+ 推薦,自動處理 null、質數乘法
        // 手寫版:return 31 * id + (name == null ? 0 : name.hashCode());
    }
}

契約鐵律(Effective Java)

  1. 一致性:a.equals(b) 為 true → a.hashCode() == b.hashCode() 必須為 true。
  2. 反向不要求:hashCode 相等,equals 可以 false(哈希碰撞)。
  3. 不可變域參與:參與 equals 的字段應為 final 或不可變,否則放入 Set/Map 作為 Key 後修改字段會導致找不到對象(桶位置變了)。

四、進階去重場景與方案

場景方案關鍵點
List 保序去重LinkedHashSet / Stream.distinct()LinkedHashSet 維護插入順序;Stream 需正確 equals/hashCode
按特定字段去重 (如 id)TreeSet + Comparator / Stream + 自定義 KeyTreeSet 用 compareTo 判斷相等,僅比較 id;或用 Map<Id, Person> 覆蓋
大數據流式去重Bloom Filter / Bitmap / 外部排序內存不足時的概率/精確算法
並發環境去重ConcurrentHashMap.keySet() / CopyOnWriteArrayList + synchronized線程安全 Set 視圖

五、Stream API 一行代碼去重 (Java 8+)


// 保序去重
List<Person> distinct = list.stream().distinct().collect(Collectors.toList());

// 按 id 去重(保留首次出現)
List<Person> distinctById = list.stream()
    .collect(Collectors.collectingAndThen(
        Collectors.toCollection(() -> new TreeSet<>(Comparator.comparing(Person::getId))),
        ArrayList::new
    ));

// 或更通用的自定義 Key 去重工具
public static <T, K> List<T> distinctByKey(List<T> list, Function<T, K> keyMapper) {
    Map<K, T> map = new LinkedHashMap<>();
    for (T t : list) map.putIfAbsent(keyMapper.apply(t), t);
    return new ArrayList<>(map.values());
}

六、總結

  1. 首選:new LinkedHashSet<>(list) (保序) 或 new HashSet<>(list) (不保序)。
  2. 前提:元素類型正確重寫 equals 和 hashCode,且基於不可變字段。
  3. 原理:Set 底層靠 HashMap Key 唯一性,判斷依據 = hashCode 相等 + equals 為真。
  4. 避坑:不要在實體類中基於可變字段實現 equals/hashCode 後再放入 HashSet/HashMap 作為 Key。

掌握這套機制,去重不再是「調用 API」,而是「設計契約」。

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