==、equals()、= 是 Java 基礎中最易混淆、也是面試高頻考點。本文從 **JVM 記憶體模型**、**語言規範**、**原始碼實作** 三個維度徹底釐清三者差異,並提供防錯速查表與最佳實踐。
| 運算子/方法 | 用途 | 比較對象 | 適用類型 | 關鍵行為 |
|---|---|---|---|---|
= | 賦值 | — | 所有 | 將右值寫入左變數儲存位置 |
== | 比較 | 數值 (基本型別) / 參考位址 (參考型別) | 所有 | 基本型別比值;參考型別比堆記憶體位址 (是否同一物件) |
equals() | 比較 | 邏輯內容 (由類別定義) | 參考型別 (Object 子類) | 預設同 == (比位址);常見類別 (String, 包裝類, 集合) 重寫比內容 |
代碼: String s1 = new String("hello");
String s2 = new String("hello");
String s3 = s1;
棧 (Stack) 堆 (Heap)
┌──────────────┐ ┌──────────────────────┐
│ s1 ──────────┼──────────────────►│ String object @0x100 │
│ │ │ value: "hello" │
│ s2 ──────────┼──────────────────►│ String object @0x200 │
│ │ │ value: "hello" │
│ s3 ──────────┼──────────────────►│ (指向 @0x100) │
└──────────────┘ └──────────────────────┘
比較結果:
s1 == s2 → false (位址 0x100 != 0x200)
s1 == s3 → true (同一位址 0x100)
s1.equals(s2) → true (內容 "hello" 相同)
s1.equals(s3) → true
// java.lang.Object
public boolean equals(Object obj) {
return (this == obj); // 直接比較參考位址
}
// java.lang.String (簡化版)
public boolean equals(Object anObject) {
if (this == anObject) return true; // 快速路徑:同一實例
if (anObject instanceof String) {
String other = (String) anObject;
// 比較長度、逐字元比較 value 陣列 (或 coder + bytes)
return this.value.length == other.value.length &&
Arrays.equals(this.value, other.value);
}
return false;
}
int a = 100, b = 100; a == b → true (數值相等) Integer i1 = 100, i2 = 100; // 自動裝箱:Integer.valueOf(100) i1 == i2 → true! (緩存 -128~127) Integer i3 = 200, i4 = 200; i3 == i4 → false! (超出緩存範圍,新建物件) i3.equals(i4) → true (比較數值)
| 場景 | 錯誤寫法 | 正確寫法 | 原因 |
|---|---|---|---|
| 字串比較 | if (str1 == "hello") | if ("hello".equals(str1)) 或 if (str1 != null && str1.equals("hello")) | 字面值可能在常量池,new String() 在堆,== 不穩定 |
| 包裝類比較 | if (num1 == num2) | if (num1 != null && num1.equals(num2)) | 緩存範圍外 == 失效;null 會拋 NPE |
| 自定義類別比較 | 未重寫 equals/hashCode | **必須重寫** equals/hashCode (IDE 自動生成) | 預設比位址,導致 Set/Map 失效 |
| 賦值 vs 比較 | if (flag = true) { } | if (flag == true) 或 if (flag) | = 是賦值,回傳賦值結果;== 才是比較 |
| 浮點數比較 | if (d1 == d2) | if (Math.abs(d1 - d2) < 0.0001) | 浮點數精度誤差 (0.1+0.2 != 0.3) |
public class User {
private Long id;
private String name;
private String email;
// Getter/Setter...
@Override
public boolean equals(Object o) {
if (this == o) return true; // 1. 同一實例快速返回
if (o == null || getClass() != o.getClass()) return false; // 2. null/類型檢查
User user = (User) o;
// 3. 核心業務欄位比較 (建議用 Objects.equals 處理 null)
return Objects.equals(id, user.id) &&
Objects.equals(name, user.name) &&
Objects.equals(email, user.email);
}
@Override
public int hashCode() {
// 4. 必須與 equals 使用相同欄位,滿足 hashCode 契約
return Objects.hash(id, name, email);
}
}
鐵律:a.equals(b) → a.hashCode() == b.hashCode()。違反將導致 HashMap、HashSet 行為異常。
// 空值安全比較
Objects.equals(a, b); // 內部: a==b ? true : (a==null ? false : a.equals(b))
// 空值安全 hashCode
Objects.hash(a, b, c); // 內部: Arrays.hashCode(new Object[]{a,b,c})
// 要求非空
Objects.requireNonNull(obj, "參數不可為空");
// 索引檢查
Objects.checkIndex(index, length);
Objects.checkFromToIndex(from, to, length);
基本型別 (int, double, boolean...):
┌─────────┐ ┌─────────┐
│ 變數 a │ │ 變數 b │
│ 值: 100 │ │ 值: 100 │
└────┬────┘ └────┬────┘
│ │
└──────┬────────┘
▼
== 比較數值位元模式 → true
參考型別 (String, User, List...):
棧 堆
┌─────────┐ ┌─────────────────────┐
│ 變數 s1 │────►│ String @0x100 "abc" │
└─────────┘ └─────────────────────┘
┌─────────┐ ┌─────────────────────┐
│ 變數 s2 │────►│ String @0x200 "abc" │
└─────────┘ └─────────────────────┘
s1 == s2 → 比較棧中位址 (0x100 vs 0x200) → false
s1.equals(s2) → 進入堆比較 @0x100 和 @0x200 內容 ("abc" vs "abc") → true
equals() 比較內容。== 比較數值。== (單例模式保證同一常量同一實例) 或 equals() 皆可。equals() 和 hashCode() (IDE Generate → equals/hashCode)。"constant".equals(var) 或 Objects.equals(a, b)。Math.abs(a-b) < epsilon。= 僅用於變數初始化/重新賦值,**勿用於條件判斷**。== 比較的是**「儲存位置的內容」**(基本型別存數值、參考型別存位址);equals() 比較的是**「業務邏輯定義的等價性」**。理解記憶體模型、掌握 JDK 核心類重寫規則、養成重寫 equals/hashCode 鐵律習慣,是寫出正確、健壯 Java 代碼的基石。