HashSet 是 Java 集合框架中「去重利器」,其底層由 HashMap 支撐。理解 add() 如何通過 hashCode() 定位桶位、equals() 判定相等,是正確實現業務對象去重、避免內存洩漏、排查性能抖動的核心鑰匙。本文從源碼切入,推導重寫規約,實戰演示「合併同類商品」業務場景。
// 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;
}
put 返回 null → 新增成功;返回舊值 → 已存在,覆蓋 valuemap.put(key, PRESENT) == null 即「桶中無 equals 相等的 key」| 規約 | 要求 | 違反後果 |
|---|---|---|
| 自反性 | x.equals(x) == true | Set.contains(x) 可能返回 false |
| 對稱性 | x.equals(y) → y.equals(x) | 跨集合操作異常 |
| 傳遞性 | x.equals(y) && y.equals(z) → x.equals(z) | 分組/去重邏輯錯亂 |
| 一致性 | 多次調用結果不變(除非修改比較字段) | 行為不可預測 |
| 非空性 | x.equals(null) == false | NPE 風險 |
| 等價對象 hashCode 必相等 | equals 相等 → hashCode 必相等 | 對象無法在 HashSet/HashMap 中正確查找(沉默失效) |
| hashCode 盡量分散 | 不相等對象 hashCode 儘量不同 | 退化為鏈表,O(n) 性能 |
@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);
}
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;
}
}
上例中 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)。
從 HashSet.add() 到 HashMap.put(),再到 hashCode/equals 規約,這條主線串起了 Java 集合去重的核心機制。掌握「業務鍵決定 equals/hashCode、可變字段隔離、優先用 Map.merge」三原則,能讓去重邏輯既正確又高效。