iT邦幫忙

2026 iThome 鐵人賽

0
Build on Google AI

把Google AI當夥伴打造個人高效Coding工作流系列 第 14 篇

第十四天Set集合與equals/hashCode核心契約,物件屬性修改導致記憶體洩漏分析

  • 分享至 

  • xImage
  •  

一、 Set集合的本質:Map的外包裝(Wrapper)

  • Set的核心特徵:不允許重複元素(Uniqueness)。
  • HashSet的底層真相:
    1. HashSet內部直接委派一個私有的HashMap實例:private transient HashMap<E, Object> map;
    2. 當我們呼叫set.add(e)時,底層其實執行的是map.put(e, PRESENT)。
    3. Key:就是放入Set的元素本身。
    4. Value:所有元素共享同一個靜態虛擬物件private static final Object PRESENT = new Object();。
    5. 判斷元素是否重複,完全依賴HashMap的Key唯一性判定邏輯。

二、 equals與hashCode的生死契約(The Contract)
Java 官方在 Object 類別中明確定義了兩者的約束規則:
* 一致性(Consistency):在物件未被修改的前提下,多次呼叫hashCode()必須返回相同的整數。
* 等價必然同雜湊(equals=>hashCode):如果兩個物件透過 equals()比較為 true,則兩者的hashCode()數值必須完全相同。
* 同雜湊不必然等價(hashCode => equals不成立):兩個物件的 hashCode()相同(發生雜湊碰撞),它們透過equals()比較不一定為true。


三、 只改寫 equals 卻未改寫 hashCode 的災難

  • 若自訂類別覆寫了 equals() 判斷邏輯,卻沿用 Object 預設的 hashCode()(基於記憶體位址):
    public class User {
    private String id;
    // 只改寫了 equals,認為只要 id 相同就是同一人
    @Override
    public boolean equals(Object o) { ... }
    // 未改寫 hashCode()
    }
  • 執行結果破壞:
    Set set = new HashSet<>();
    User u1 = new User("A101");
    User u2 = new User("A101");
    set.add(u1);
    set.add(u2);
    System.out.println(set.size()); // 預期是 1,實際輸出卻是 2!
  • 底層失效分析:
    1. HashSet.add() 依靠 hashCode() 計算桶位(Bucket Index)。
    2. 因未改寫 hashCode(),u1 與 u2 的預設雜湊值不同,被分配到了不同的桶位。
    3. JVM 根本不會去呼叫 u1.equals(u2) 進行內容比對,直接將兩者都插入集合中,導致 Set 的唯一性約束徹底崩潰。

四、 核心架構地雷:修改 Set 中的元素引發記憶體洩漏(Memory Leak)
這是 Java 最經典、最難排查的隱蔽 Bug 之一。

  1. 地雷重現場景:
    public class Point {
    private int x;
    private int y;

    public Point(int x, int y) { this.x = x; this.y = y; }

    @Override
    public boolean equals(Object o) {
    if (this == o) return true;
    if (!(o instanceof Point p)) return false;
    return x == p.x && y == p.y;
    }

    @Override
    public int hashCode() {
    return Objects.hash(x, y); // 雜湊值與 x, y 綁定
    }

    public void setX(int x) { this.x = x; } // 提供 Setter 允許修改內部狀態
    }

  2. 崩潰後果剖析:

  • set.contains(p) 返回 false:

    1. 當我們查詢 p 時,系統用修改後的狀態 (99, 1) 計算出全新的 Hash 值,指向了 Bucket B(假設 Index = 11)。
    2. JVM 在 Bucket B 找不到任何東西,直接判定不存在!
  • set.remove(p) 失敗:
    刪除時同樣去 Bucket B 尋找,結果刪除失敗,返回 false。

  • 幽靈物件與記憶體洩漏(Memory Leak):

    1. 該物件真實存在於 Bucket A 中,但因為其欄位已變更,外界再也無法透過正常 API 找到或刪除它。
    2. 只要 Set 還活著,這個無法存取的物件就永遠被內部陣列引用,GC 永遠無法回收它,長久累積終將導致 OutOfMemoryError。

五、 工程防禦三大鐵律

  • 不可變物件作為 Key / Set 元素(Immutability):
    放入 HashSet 或作為 HashMap Key 的物件,參與 hashCode() 計算的欄位必須宣告為 final。
    絕不在加入 Set 後呼叫 Setter 修改內部欄位;若需修改,應先從 Set 移除,修改後再重新加入。
  • 重寫成對(Pairing Rule):
    只要改寫 equals(),就必須同時改寫 hashCode()。
    兩者比對與計算的欄位集合必須保持嚴格一致。
  • 優先善用現代 Java 特性:
    盡量使用 Java 14+ 的 record 來封裝複合 Key 或 Set 元素。
    record 預設不可變(欄位全為 private final),且自動由編譯器生成嚴謹合規的 equals()、hashCode() 與 toString(),天然免疫上述記憶體洩漏陷阱。

上一篇
第十三天HashMap雜湊演算法、容量2的N次方原理與JDK 8紅黑樹轉換機制
下一篇
第十五天LinkedHashMap的LRU快取機制與TreeMap紅黑樹排序原理
系列文
把Google AI當夥伴打造個人高效Coding工作流 共 18 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言