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

HashSet 深度解析:從 add() 源碼看 hashCode/equals 重寫規約與業務去重實戰

     編輯:Java 綜合教學

HashSet 深度解析:從 add() 源碼看 hashCode/equals 重寫規約與業務去重實戰

HashSet 是 Java 集合框架中「去重利器」,其底層由 HashMap 支撐。理解 add() 如何通過 hashCode() 定位桶位、equals() 判定相等,是正確實現業務對象去重、避免內存洩漏、排查性能抖動的核心鑰匙。本文從源碼切入,推導重寫規約,實戰演示「合併同類商品」業務場景。

一、 源碼級剖析:add() 到底做了什麼?

// HashSet.add(E e)
public boolean add(E e) {
    return map.put(e, PRESENT) == null; // PRESENT = new Object()
}

// HashMap.put(K key, V value) 核心邏輯 (JDK 8 簡化)
public V put(K key, V value) {
    if (key == null) return putForNullKey(value);
    int hash = hash(key);                    // 1. 計算 hash
    int i = indexFor(hash, table.length);    // 2. 定位桶索引
    for (Entry e = table[i]; e != null; e = e.next) { // 3. 遍歷鏈表/紅黑樹
        Object k;
        if (e.hash == hash &&                // hash 相等
            ((k = e.key) == key || key.equals(k))) {       // 且 equals 相等
            V oldValue = e.value;
            e.value = value;                 // 覆蓋 value
            return oldValue;
        }
    }
    // 4. 無匹配節點 → 新增 Entry
    addEntry(hash, key, value, i);
    return null;
}

關鍵推論

  1. hashCode() 決定「去哪個桶」:hash 值相同 → 同一桶(鏈表/樹)
  2. equals() 決定「是不是同一個」:同桶下逐個 equals 比較
  3. 返回值語義:put 返回 null → 新增成功;返回舊值 → 已存在,覆蓋 value
  4. HashSet 去重本質:map.put(key, PRESENT) == null 即「桶中無 equals 相等的 key」

二、 hashCode/equals 重寫鐵律(Effective Java 條目 10-11)

規約要求違反後果
自反性x.equals(x) == trueSet.contains(x) 可能返回 false
對稱性x.equals(y) → y.equals(x)跨集合操作異常
傳遞性x.equals(y) && y.equals(z) → x.equals(z)分組/去重邏輯錯亂
一致性多次調用結果不變(除非修改比較字段)行為不可預測
非空性x.equals(null) == falseNPE 風險
等價對象 hashCode 必相等equals 相等 → hashCode 必相等對象無法在 HashSet/HashMap 中正確查找(沉默失效)
hashCode 盡量分散不相等對象 hashCode 儘量不同退化為鏈表,O(n) 性能

三、 IDE 生成模板 vs 手寫優化

IDEA 默認生成(基於所有非靜態非 transient 字段)

@Override
public boolean equals(Object o) {
    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;
    Goods goods = (Goods) o;
    return count == goods.count &&
           wmsId == goods.wmsId &&
           Objects.equals(skuNo, goods.skuNo);
}

@Override
public int hashCode() {
    return Objects.hash(skuNo, wmsId, count);
}

業務場景優化:以「skuNo + wmsId」為業務鍵,忽略 count

public class WarehouseGoods {
    private String skuNo;   // 商品編碼
    private int wmsId;      // 倉庫ID
    private int count;      // 數量(可變)

    // 業務鍵:同倉庫同商品視為同一條記錄
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        WarehouseGoods that = (WarehouseGoods) o;
        return wmsId == that.wmsId && Objects.equals(skuNo, that.skuNo);
    }

    @Override
    public int hashCode() {
        return Objects.hash(skuNo, wmsId); // 僅業務鍵參與 hash
    }

    // 輔助方法:合併數量
    public void mergeCount(WarehouseGoods other) {
        this.count += other.count;
    }
}

四、 實戰:合併同倉庫同商品記錄

public class MergeDemo {
    public static void main(String[] args) {
        Set set = new HashSet<>();

        WarehouseGoods g1 = new WarehouseGoods("SKU001", 1, 10);
        WarehouseGoods g2 = new WarehouseGoods("SKU001", 1, 5);  // 同倉同商品
        WarehouseGoods g3 = new WarehouseGoods("SKU002", 1, 20);
        WarehouseGoods g4 = new WarehouseGoods("SKU001", 2, 8);  // 不同倉庫

        // 利用 HashSet 去重特性 + 手動合併
        for (WarehouseGoods g : Arrays.asList(g1, g2, g3, g4)) {
            // 嘗試查找已存在對象
            WarehouseGoods existing = findInSet(set, g);
            if (existing != null) {
                existing.mergeCount(g); // 合併數量
                System.out.println("合併: " + existing);
            } else {
                set.add(g);
                System.out.println("新增: " + g);
            }
        }
        // 輸出: 新增 SKU001@w1(10) → 合併 SKU001@w1(15) → 新增 SKU002@w1(20) → 新增 SKU001@w2(8)
    }

    // HashSet 無直接 get 方法,需遍歷查找(或改用 HashMap)
    private static WarehouseGoods findInSet(Set set, WarehouseGoods target) {
        for (WarehouseGoods g : set) {
            if (g.equals(target)) return g;
        }
        return null;
    }
}

五、 進階:為什麼不直接用 HashMap<BusinessKey, Entity>?

上例中 findInSet 是 O(n) 遍歷。高性能場景應直接用 HashMap:

// 業務鍵類(不可變、final、正確重寫 hashCode/equals)
record BusinessKey(String skuNo, int wmsId) {}

Map map = new HashMap<>();
for (WarehouseGoods g : goodsList) {
    BusinessKey key = new BusinessKey(g.getSkuNo(), g.getWmsId());
    map.merge(key, g, (old, nw) -> { old.mergeCount(nw); return old; });
}

Map.merge (JDK 8) 原子化「查找-合併-放回」,無需手寫查找邏輯,性能 O(1)。

六、 避坑清單

  1. 只重寫 equals 不重寫 hashCode → 違反規約,對象在 HashSet 中「查無此人」
  2. hashCode 包含可變字段 → 修改字段後 hash 改變,對象「丟失」在錯誤桶中
  3. equals 中使用 instanceof 而非 getClass() → 破壞對稱性(子類實例可能等於父類)
  4. 大量對象 hashCode 衝突 → 退化鏈表,需優化算法(如引入質數乘法、高位混合)
  5. 將可變對象作為 Set 元素/Map Key → 修改字段導致結構錯亂,**Key 必須不可變**

從 HashSet.add() 到 HashMap.put(),再到 hashCode/equals 規約,這條主線串起了 Java 集合去重的核心機制。掌握「業務鍵決定 equals/hashCode、可變字段隔離、優先用 Map.merge」三原則,能讓去重邏輯既正確又高效。

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