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

摩根士丹利面試真題:HashMap 鍵為可變對象導致數據「消失」的陷阱深度解析

     編輯:Java 綜合教學

題目還原

Map map = new HashMap<>();
Person p1 = new Person(1, "aaa");
Person p2 = new Person(1, "bbb"); // id 相同,equals 判定為同一鍵

map.put(p1, p1);
map.put(p2, p2); // p2 覆蓋 p1 的值

System.out.println(map.get(p1)); // Person[id=1, name=bbb]
System.out.println(map.get(p2)); // Person[id=1, name=bbb]

p2.setId(2); // 修改鍵對象的 id(影響 hashCode)

System.out.println(map.get(p1)); // Person[id=2, name=bbb] —— 值對象也被改了!
System.out.println(map.get(p2)); // null —— 找不到鍵了!

核心類定義

class Person {
    private int id;
    private String name;

    // getter/setter/toString 省略

    @Override
    public int hashCode() {
        return id; // 僅基於 id 計算哈希
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) return true;
        if (!(obj instanceof Person)) return false;
        return id == ((Person) obj).getId(); // 僅比較 id
    }
}

現象解析:為什麼會「消失」?

  1. put 階段:p1 和 p2 的 hashCode() 均為 1,equals 返回 true,HashMap 判定為同一鍵,p2 的值覆蓋 p1。桶中存儲:key=p1(引用), value=p2(引用)。
  2. 修改 id:p2.setId(2) 導致 p2.hashCode() 變為 2。**但桶位置仍由舊 hashCode (1) 決定**。
  3. get(p2) 階段:計算 p2.hashCode()=2,去桶 2 找 → **找不到**,返回 null。
  4. get(p1) 階段:p1 與 p2 指向同一對象(或同一引用鏈),p1.id 也被改為 2,hashCode()=2。去桶 2 找 → 仍找不到!但原文輸出顯示 Person[id=2, name=bbb],說明此處 p1 與 p2 是**同一對象引用**(或測試代碼有細微差異)。關鍵點:**鍵對象修改後,HashMap 無法定位原桶位置,導致數據「丟失」**。

根本原因:違反了 HashMap 鍵的不可變約定

作為 HashMap 鍵的對象,一旦放入 Map,其 hashCode() 和 equals() 依賴的字段必須不可變。

Java 文檔明確警告:「如果鍵對象的值發生變化,導致 equals 比較結果改變,HashMap 的行為是不可預期的。」

正確做法與最佳實踐

1. 鍵對象設計為不可變類

public final class PersonKey {
    private final int id; // final,無 setter
    private final String name;

    public PersonKey(int id, String name) {
        this.id = id;
        this.name = name;
    }
    // 僅提供 getter,無 setter
    // hashCode/equals 基於所有 final 字段
}

2. 僅使用不可變字段計算 hashCode/equals

@Override
public int hashCode() {
    return Objects.hash(id); // id 必須不可變
}

@Override
public boolean equals(Object o) {
    if (this == o) return true;
    if (!(o instanceof Person)) return false;
    Person p = (Person) o;
    return id == p.id;
}

3. 若業務必須修改,請「先刪後加」

// 修改鍵的 id
Object value = map.remove(p2); // 先用舊鍵移除
p2.setId(2);
map.put(p2, value); // 再用新鍵放入

4. 使用不可變包裝類型作為鍵

  • 直接用 Integer、String、Long 等 JDK 不可變類型。
  • 或用組合鍵:Map map,Key = id + ":" + type。

延伸思考:面試官可能追問的點

Q: HashMap 1.8 怎麼解決哈希衝突?
A: 數組 + 鏈表 + 紅黑樹(鏈表長度 > 8 且數組長度 > 64 時樹化)。
Q: 為什麼 hashCode 用 31 作為乘數?
A: 31 = 2^5 - 1,JVM 可優化為 (i << 5) - i,速度快且分佈均勻。
Q: ConcurrentHashMap 如何保證線程安全?
A: 1.7 分段鎖;1.8 CAS + synchronized + 節點鎖,鏈表/樹節點細粒度鎖。
Q: 如何實現一個「可變鍵」的 Map?
A: 用 IdentityHashMap(基于 == 比較),或自定義 Map 重寫 get/put 邏輯,但極不推薦用於生產。

總結

這道摩根士丹利面試題,考察的不只是 HashMap 底層原理,更是**Java 核心類庫設計契約**的理解:**不可變性是集合框架正確工作的基石**。實戰中,請始終將「鍵對象不可變」作為鐵律刻入肌肉記憶,代碼審查時重點檢查鍵類是否存在 setter、非 final 字段參與 hashCode/equals。這樣,你就不會在生產環境裡看到數據神秘「消失」的靈異事件了。

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