在 Java 集合框架中,正確重寫 `equals()` 和 `hashCode()` 是將自定義對象作為 `HashMap` Key、存入 `HashSet` 的前提。本文通過一個經典失敗案例,深度解析兩者契約、底層原理及正確實現姿勢。
class Apple {
private String color;
public Apple(String color) { this.color = color; }
// 重寫 equals,未重寫 hashCode
public boolean equals(Object obj) {
if (!(obj instanceof Apple)) return false;
if (obj == this) return true;
return this.color.equals(((Apple) obj).color);
}
}
public static void main(String[] args) {
Map<Apple, Integer> map = new HashMap<>();
Apple a1 = new Apple("green");
map.put(a1, 10); // 存入
System.out.println(map.get(new Apple("green"))); // 輸出 null!為什麼?
}
現象:明明存入了 "green" 蘋果,用同樣顏色的新對象取值卻返回 `null`。調試發現 Map 內確實有該對象。
1. 如果兩個對象 equals() 返回 true,則它們的 hashCode() 必須相等。
2. 如果兩個對象 hashCode() 相等,它們 equals() 不一定為 true (允許哈希碰撞)。
違反契約 1 會導致「等價對象落入不同桶」,集合無法正常工作。
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Apple apple = (Apple) o;
return Objects.equals(color, apple.color);
}
@Override
public int hashCode() {
return Objects.hash(color); // 自動處理 null,生成合理哈希
}
@Override
public int hashCode() {
int result = 17;
result = 31 * result + (color == null ? 0 : color.hashCode());
return result;
}
哈希分桶是為了 O(1) 平均查找性能。將對象盡量均勻分散到不同桶 (像貨物分倉庫),避免所有對象擠在一個桶退化為 O(n) 線性查找。`hashCode()` 決定「哪個倉庫」,`equals()` 決定「是不是目標貨物」。