比較兩個物件相等這種事,看似簡單但超容易踩坑。特別是涉及 equals 和 hashCode 時,小細節搞錯就會爆。見過太多人只覆寫 equals 不覆寫 hashCode,或用 BigDecimal 的 equals 爆掉的慘案。

核心概念

要比較兩個物件相等,**必須同時覆寫 equals() 和 hashCode()**。

為什麼?因為 HashMap、HashSet 等容器依賴 hashCode 和 equals 配合:

1
2
3
4
5
6
7
8
9
// 容器先用 hashCode 定位,再用 equals 驗證
Map<User, String> map = new HashMap<>();
User u1 = new User(1L, "Alice");
map.put(u1, "value1");

User u2 = new User(1L, "Alice");
// 如果只覆寫 equals 不覆寫 hashCode:
// u1 和 u2 hashCode 不同 → 容器認為是不同的鍵
// map.get(u2) 會回傳 null!

正確的 equals 實作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class User {
private Long id;
private String name;
private Integer age;

@Override
public boolean equals(Object obj) {
// 1. 檢查是否同一物件
if (this == obj) {
return true;
}

// 2. 檢查 null
if (obj == null) {
return false;
}

// 3. 檢查類別是否相同
if (this.getClass() != obj.getClass()) {
return false;
}

// 4. 轉型並比較各個屬性
User other = (User) obj;
return Objects.equals(this.id, other.id) &&
Objects.equals(this.name, other.name) &&
Objects.equals(this.age, other.age);
}
}

關鍵點:

  • this == obj - 最快的路徑,同物件當然相等
  • obj == null - null 不等於任何物件
  • getClass() - 不用 instanceof,因為 instanceof 允許子類別,容易出問題
  • Objects.equals() - 自動處理 null,安全

正確的 hashCode 實作

1
2
3
4
@Override
public int hashCode() {
return Objects.hash(this.id, this.name, this.age);
}

就這麼簡單。Objects.hash() 已經幫你処理好一切。

但要注意:hashCode 和 equals 要一致

1
2
3
4
5
6
// 雙重檢查:如果 a.equals(b) 為 true,a.hashCode() 必須等於 b.hashCode()
User u1 = new User(1L, "Alice", 30);
User u2 = new User(1L, "Alice", 30);

assertTrue(u1.equals(u2));
assertEquals(u1.hashCode(), u2.hashCode()); // 必須相等

實際完整範例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
public class User {
private Long id;
private String name;
private Integer age;

public User(Long id, String name, Integer age) {
this.id = id;
this.name = name;
this.age = age;
}

@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null) return false;
if (this.getClass() != obj.getClass()) return false;

User other = (User) obj;
return Objects.equals(this.id, other.id) &&
Objects.equals(this.name, other.name) &&
Objects.equals(this.age, other.age);
}

@Override
public int hashCode() {
return Objects.hash(this.id, this.name, this.age);
}

@Override
public String toString() {
return "User{" + "id=" + id + ", name='" + name + '\'' + ", age=" + age + '}';
}

// getter/setter...
}

// 使用
User u1 = new User(1L, "Alice", 30);
User u2 = new User(1L, "Alice", 30);

System.out.println(u1.equals(u2)); // true
System.out.println(u1 == u2); // false (不同物件)

Set<User> users = new HashSet<>();
users.add(u1);
users.add(u2);
System.out.println(users.size()); // 1 (因為 equals 相等,只儲存一個)

Map<User, String> map = new HashMap<>();
map.put(u1, "value1");
System.out.println(map.get(u2)); // "value1" (能夠 get 到)

踩坑 1:陣列屬性

如果物件有陣列欄位,要用 Arrays.equals 和 Arrays.hashCode:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Config {
private String name;
private int[] values;

@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null || this.getClass() != obj.getClass()) return false;

Config other = (Config) obj;
return Objects.equals(this.name, other.name) &&
Arrays.equals(this.values, other.values); // 用 Arrays.equals
}

@Override
public int hashCode() {
return Objects.hash(this.name, Arrays.hashCode(this.values));
}
}

踩坑 2:BigDecimal 的 equals 地雷

這是超常見的坑!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class Payment {
private Long id;
private BigDecimal amount;

// 錯誤做法
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null || this.getClass() != obj.getClass()) return false;

Payment other = (Payment) obj;
// 直接用 equals 會比較 scale!
return Objects.equals(this.id, other.id) &&
Objects.equals(this.amount, other.amount);
}
}

// 爆掉的例子
BigDecimal b1 = new BigDecimal("10.00");
BigDecimal b2 = new BigDecimal("10");

System.out.println(b1.equals(b2)); // false!因為 scale 不同

Payment p1 = new Payment(1L, b1);
Payment p2 = new Payment(1L, b2);
System.out.println(p1.equals(p2)); // false,這不是你想要的

正確做法:用 compareTo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class Payment {
private Long id;
private BigDecimal amount;

@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null || this.getClass() != obj.getClass()) return false;

Payment other = (Payment) obj;
return Objects.equals(this.id, other.id) &&
this.amount.compareTo(other.amount) == 0; // 用 compareTo
}

@Override
public int hashCode() {
// 注意:compareTo 相等不代表 hashCode 要相等
// 所以這邊用 stripTrailingZeros().hashCode()
return Objects.hash(
this.id,
this.amount.stripTrailingZeros().hashCode()
);
}
}

// 現在 OK
BigDecimal b1 = new BigDecimal("10.00");
BigDecimal b2 = new BigDecimal("10");

Payment p1 = new Payment(1L, b1);
Payment p2 = new Payment(1L, b2);
System.out.println(p1.equals(p2)); // true

踩坑 3:DB 取出的 BigDecimal 帶小數位

這個坑常發生在 ORM(Hibernate、MyBatis)場景:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// DB 中存 DECIMAL(10, 2)
// 即使 DB 存的是 10,取出來可能是 10.00(scale = 2)

@Entity
@Table(name = "products")
public class Product {
@Id
private Long id;

@Column(precision = 10, scale = 2)
private BigDecimal price; // DB: 99.50,Java: BigDecimal("99.50")
}

// 程式中比較時,一定要設定 scale
public class PriceComparison {

public boolean isSamePrice(BigDecimal dbPrice, BigDecimal inputPrice) {
// 錯誤 - 可能因為 scale 不同而比較失敗
// return dbPrice.equals(inputPrice);

// 正確 - 先統一 scale
return dbPrice.setScale(2, RoundingMode.HALF_UP)
.compareTo(inputPrice.setScale(2, RoundingMode.HALF_UP)) == 0;
}

// 或在 entity 的 getter 自動處理
public BigDecimal getPrice() {
return price != null ? price.setScale(2, RoundingMode.HALF_UP) : null;
}
}

踩坑 4:只覆寫 equals 不覆寫 hashCode

最危險的做法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class BuggyUser {
private Long id;

@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null || this.getClass() != obj.getClass()) return false;
BuggyUser other = (BuggyUser) obj;
return Objects.equals(this.id, other.id);
}

// 沒有覆寫 hashCode!使用預設的物件指針雜湊
}

// 災難發生
Set<BuggyUser> set = new HashSet<>();
BuggyUser u1 = new BuggyUser(1L);
BuggyUser u2 = new BuggyUser(1L);

set.add(u1);
set.add(u2);

System.out.println(set.size()); // 2!應該是 1
// 因為 hashCode 不同,set 認為它們是不同的物件,都加進去了

踩坑 5:mutable 物件當 HashMap key

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// 危險的做法
public class MutableUser {
private Long id;
private String name;

public void setName(String name) { // 可以修改!
this.name = name;
}

@Override
public boolean equals(Object obj) { ... }

@Override
public int hashCode() {
return Objects.hash(this.id, this.name);
}
}

Map<MutableUser, String> map = new HashMap<>();
MutableUser user = new MutableUser(1L, "Alice");
map.put(user, "data1");

user.setName("Bob"); // 修改了 hashCode!
System.out.println(map.get(user)); // null!
// 因為修改後 hashCode 變了,容器無法找到

// 最佳實踐:key 應該是 immutable 的
public final class ImmutableUser {
private final Long id;
private final String name;

public ImmutableUser(Long id, String name) {
this.id = id;
this.name = name;
}

// 沒有 setter
}

IDE 自動生成

大多數 IDE 都能幫你生成 equals 和 hashCode:

IntelliJ IDEA: Code → Generate → equals() and hashCode()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// IntelliJ 會生成類似這樣的程式碼
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
User user = (User) o;
return Objects.equals(id, user.id) &&
Objects.equals(name, user.name) &&
Objects.equals(age, user.age);
}

@Override
public int hashCode() {
return Objects.hash(id, name, age);
}

基本上就用 IDE 的,很少需要手寫。

總結檢查清單

覆寫 equals 和 hashCode 時,檢查:

  • equals 先檢查 this == obj
  • equals 檢查 null
  • equals 用 getClass() 檢查類別,不用 instanceof
  • 陣列欄位用 Arrays.equals
  • BigDecimal 用 compareTo,不用 equals
  • hashCode 用 Objects.hash()
  • 有陣列用 Arrays.hashCode()
  • a.equals(b) 為 true 時,a.hashCode() == b.hashCode()
  • key 物件 immutable

最後的話

equals 和 hashCode 這個梗,每個 Java 開發者都踩過。掌握正確的實作方式,使用 IDE 生成,特別留意 BigDecimal 和陣列的坑,就基本不會出問題。線上爆掉一次真的會疼,預防勝於治療。