比較兩個物件相等這種事,看似簡單但超容易踩坑。特別是涉及 equals 和 hashCode 時,小細節搞錯就會爆。見過太多人只覆寫 equals 不覆寫 hashCode,或用 BigDecimal 的 equals 爆掉的慘案。
核心概念
要比較兩個物件相等,**必須同時覆寫 equals() 和 hashCode()**。
為什麼?因為 HashMap、HashSet 等容器依賴 hashCode 和 equals 配合:
1 2 3 4 5 6 7 8 9
| Map<User, String> map = new HashMap<>(); User u1 = new User(1L, "Alice"); map.put(u1, "value1");
User u2 = new User(1L, "Alice");
|
正確的 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) { 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); } }
|
關鍵點:
- 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
| 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 + '}'; } }
User u1 = new User(1L, "Alice", 30); User u2 = new User(1L, "Alice", 30);
System.out.println(u1.equals(u2)); System.out.println(u1 == u2);
Set<User> users = new HashSet<>(); users.add(u1); users.add(u2); System.out.println(users.size());
Map<User, String> map = new HashMap<>(); map.put(u1, "value1"); System.out.println(map.get(u2));
|
踩坑 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); } @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; 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));
Payment p1 = new Payment(1L, b1); Payment p2 = new Payment(1L, b2); System.out.println(p1.equals(p2));
|
正確做法:用 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; } @Override public int hashCode() { return Objects.hash( this.id, this.amount.stripTrailingZeros().hashCode() ); } }
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));
|
踩坑 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
|
@Entity @Table(name = "products") public class Product { @Id private Long id; @Column(precision = 10, scale = 2) private BigDecimal price; }
public class PriceComparison { public boolean isSamePrice(BigDecimal dbPrice, BigDecimal inputPrice) { return dbPrice.setScale(2, RoundingMode.HALF_UP) .compareTo(inputPrice.setScale(2, RoundingMode.HALF_UP)) == 0; } 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); } }
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());
|
踩坑 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"); System.out.println(map.get(user));
public final class ImmutableUser { private final Long id; private final String name; public ImmutableUser(Long id, String name) { this.id = id; this.name = name; } }
|
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
| @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 和 hashCode 這個梗,每個 Java 開發者都踩過。掌握正確的實作方式,使用 IDE 生成,特別留意 BigDecimal 和陣列的坑,就基本不會出問題。線上爆掉一次真的會疼,預防勝於治療。
你的鼓勵將被轉換為我明天繼續加班的動力(真的)。 ❤️