Java中的==、equals與hashCode區(qū)別與聯(lián)系最佳實(shí)踐
在Java開發(fā)中,==、equals和hashCode是三個(gè)高頻出現(xiàn)的概念,也是初學(xué)者最容易混淆的知識(shí)點(diǎn)。它們看似簡(jiǎn)單,卻蘊(yùn)含著Java對(duì)象模型和哈希表設(shè)計(jì)的深層邏輯。本文將從底層原理出發(fā),全面解析三者的區(qū)別、聯(lián)系及最佳實(shí)踐,幫你徹底理清它們的使用場(chǎng)景。
一、==:比較的是"身份"還是"內(nèi)容"?
==是Java中的運(yùn)算符,用于比較兩個(gè)變量的值。但它的行為會(huì)因比較的類型不同而產(chǎn)生差異,核心區(qū)別在于基本數(shù)據(jù)類型和引用數(shù)據(jù)類型的比較邏輯。
1. 基本數(shù)據(jù)類型:比較"值"本身
Java中的基本數(shù)據(jù)類型(byte、short、int、long、float、double、char、boolean)直接存儲(chǔ)值,不存在"引用"的概念。因此,==比較的是兩個(gè)變量存儲(chǔ)的實(shí)際值是否相等。
int a = 10; int b = 10; System.out.println(a == b); // true(值相同) char c1 = 'A'; char c2 = 65; // 'A'的ASCII碼是65 System.out.println(c1 == c2); // true(值相同)
2. 引用數(shù)據(jù)類型:比較"內(nèi)存地址"
引用數(shù)據(jù)類型(如String、Object、自定義類等)的變量存儲(chǔ)的是對(duì)象在堆內(nèi)存中的地址(引用)。因此,==比較的是兩個(gè)變量是否指向同一個(gè)對(duì)象(即內(nèi)存地址是否相同)。
String s1 = new String("hello");
String s2 = new String("hello");
System.out.println(s1 == s2); // false(兩個(gè)不同的對(duì)象,地址不同)
String s3 = s1;
System.out.println(s1 == s3); // true(s3和s1指向同一個(gè)對(duì)象)關(guān)鍵結(jié)論:==對(duì)于基本類型是"值比較",對(duì)于引用類型是"地址比較"(判斷是否為同一對(duì)象)。
二、equals:對(duì)象內(nèi)容的比較器
equals是Object類定義的方法,用于判斷兩個(gè)對(duì)象是否"相等"。與==不同,equals的邏輯可以由開發(fā)者自定義,默認(rèn)行為與==一致,但多數(shù)類會(huì)重寫它以實(shí)現(xiàn)"內(nèi)容比較"。
1. Object類的默認(rèn)equals實(shí)現(xiàn)
Object類中equals的源碼如下:
public boolean equals(Object obj) {
return (this == obj); // 本質(zhì)是用==比較,即比較內(nèi)存地址
}這意味著:如果一個(gè)類沒有重寫equals,那么它的equals方法與==完全等價(jià),比較的是對(duì)象的內(nèi)存地址。
class Person {
private String name;
// 省略構(gòu)造方法和getter
}
Person p1 = new Person("張三");
Person p2 = new Person("張三");
System.out.println(p1.equals(p2)); // false(未重寫equals,等價(jià)于==)2. 重寫equals:實(shí)現(xiàn)"內(nèi)容相等"
實(shí)際開發(fā)中,我們通常認(rèn)為"內(nèi)容相同的對(duì)象應(yīng)該相等"(如兩個(gè)String的字符序列相同即為相等)。因此,許多Java內(nèi)置類(如String、Integer、List)都重寫了equals方法。
例1:String類的equals實(shí)現(xiàn)
String重寫的equals用于比較字符序列是否相同:
public boolean equals(Object anObject) {
if (this == anObject) {
return true; // 同一對(duì)象,直接返回true
}
if (anObject instanceof String) {
String anotherString = (String)anObject;
int n = value.length;
if (n == anotherString.value.length) {
char v1[] = value;
char v2[] = anotherString.value;
int i = 0;
while (n-- != 0) { // 逐個(gè)比較字符
if (v1[i] != v2[i])
return false;
i++;
}
return true;
}
}
return false;
}使用示例:
String s1 = new String("hello");
String s2 = new String("hello");
System.out.println(s1.equals(s2)); // true(內(nèi)容相同)
System.out.println(s1 == s2); // false(地址不同)例2:自定義類重寫equals
重寫equals需遵循等價(jià)關(guān)系的規(guī)則(否則會(huì)導(dǎo)致邏輯混亂):
- 自反性:
x.equals(x)必須返回true; - 對(duì)稱性:若
x.equals(y)為true,則y.equals(x)也必須為true; - 傳遞性:若
x.equals(y)和y.equals(z)為true,則x.equals(z)必須為true; - 一致性:多次調(diào)用
x.equals(y),結(jié)果應(yīng)保持一致; - 非空性:
x.equals(null)必須返回false。
正確重寫Person類的equals示例:
class Person {
private String name;
private int age;
// 構(gòu)造方法、getter省略
@Override
public boolean equals(Object o) {
if (this == o) return true; // 同一對(duì)象,直接返回true
if (o == null || getClass() != o.getClass()) return false; // 類型不同或null,返回false
Person person = (Person) o;
// 比較關(guān)鍵屬性(name和age都相同才認(rèn)為相等)
return age == person.age && Objects.equals(name, person.name);
}
}關(guān)鍵結(jié)論:equals的默認(rèn)行為是比較對(duì)象地址(與==一致),但可通過重寫實(shí)現(xiàn)"內(nèi)容比較",其邏輯由開發(fā)者定義(通?;趯?duì)象的關(guān)鍵屬性)。
三、hashCode:哈希表的"定位器"
hashCode是Object類的另一個(gè)方法,返回一個(gè)int類型的哈希碼(散列值)。它的核心作用是輔助哈希表(如HashMap、HashSet)快速定位對(duì)象,是哈希表高效運(yùn)作的基礎(chǔ)。
1. hashCode的本質(zhì)與作用
哈希碼是對(duì)象的"數(shù)字指紋",由對(duì)象的內(nèi)部狀態(tài)計(jì)算得出。在哈希表中,它的作用是:
- 快速確定對(duì)象在哈希表中的存儲(chǔ)位置(通過哈希碼計(jì)算"桶位");
- 減少
equals的調(diào)用次數(shù)(先通過哈希碼篩選,再用equals精確比較)。
Object類中hashCode的默認(rèn)實(shí)現(xiàn)是根據(jù)對(duì)象的內(nèi)存地址計(jì)算哈希碼(不同對(duì)象的哈希碼通常不同),但子類可以重寫它。
2. 重寫hashCode的規(guī)則
與equals類似,hashCode也需要遵循一定的規(guī)則,尤其是當(dāng)類重寫了equals時(shí):
- 一致性:同一對(duì)象多次調(diào)用
hashCode(),必須返回相同的整數(shù)(對(duì)象狀態(tài)未修改時(shí)); - 等價(jià)性:若
a.equals(b) == true,則a.hashCode()必須等于b.hashCode(); - 非必須等價(jià):若
a.equals(b) == false,a.hashCode()與b.hashCode()可以相等(即允許哈希沖突)。
為什么規(guī)則2如此重要?
如果兩個(gè)對(duì)象equals返回true但hashCode不同,在哈希表中會(huì)被分配到不同的桶位,導(dǎo)致哈希表認(rèn)為它們是不同的對(duì)象,從而破壞哈希表的邏輯(如HashSet中出現(xiàn)重復(fù)元素)。
3. 如何正確重寫hashCode?
重寫hashCode的核心原則是:根據(jù)equals中用于比較的所有屬性計(jì)算哈希碼,確保"相等的對(duì)象有相同的哈希碼"。
Java提供了Objects.hash()工具方法,可便捷地生成哈希碼(內(nèi)部通過組合各屬性的哈希值實(shí)現(xiàn))。
為前面的Person類重寫hashCode:
@Override
public int hashCode() {
// 基于name和age計(jì)算哈希碼(與equals中比較的屬性一致)
return Objects.hash(name, age);
}Objects.hash()的簡(jiǎn)化原理:
public static int hash(Object... values) {
int result = 1;
for (Object element : values) {
result = 31 * result + (element == null ? 0 : element.hashCode());
}
return result;
}選擇31作為乘數(shù)的原因:31是質(zhì)數(shù),且31 * i = (i << 5) - i,可通過移位運(yùn)算高效計(jì)算。
四、==、equals與hashCode的關(guān)聯(lián)關(guān)系
三者并非孤立存在,尤其是equals和hashCode,在哈希表場(chǎng)景中存在強(qiáng)關(guān)聯(lián),我們可以用一句話總結(jié):
==判斷是否為同一對(duì)象;equals判斷內(nèi)容是否相等;hashCode輔助哈希表快速查找,且必須與equals保持邏輯一致。
具體關(guān)聯(lián)如下:
1. ==與equals的關(guān)系
- 若
a == b為true,則a.equals(b)一定為true(同一對(duì)象,內(nèi)容必然相同); - 若
a.equals(b)為true,a == b不一定為true(內(nèi)容相同的不同對(duì)象)。
例如:
String s1 = "hello";
String s2 = "hello"; // 常量池復(fù)用,s1和s2指向同一對(duì)象
System.out.println(s1 == s2); // true
System.out.println(s1.equals(s2)); // true
String s3 = new String("hello");
System.out.println(s1 == s3); // false(不同對(duì)象)
System.out.println(s1.equals(s3)); // true(內(nèi)容相同)2. equals與hashCode的強(qiáng)制約束
這是開發(fā)中最容易出錯(cuò)的點(diǎn),必須牢記:
- 若
a.equals(b) = true,則a.hashCode()必須等于b.hashCode()(否則哈希表會(huì)出錯(cuò)); - 若
a.hashCode() = b.hashCode(),a.equals(b)可能為false(哈希沖突是允許的)。
反例(違反約束會(huì)導(dǎo)致的問題):
class BadPerson {
private String name;
public BadPerson(String name) { this.name = name; }
// 只重寫equals,未重寫hashCode
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
BadPerson badPerson = (BadPerson) o;
return Objects.equals(name, badPerson.name);
}
}
// 測(cè)試代碼
public class Test {
public static void main(String[] args) {
BadPerson p1 = new BadPerson("張三");
BadPerson p2 = new BadPerson("張三");
System.out.println(p1.equals(p2)); // true(內(nèi)容相同)
System.out.println(p1.hashCode() == p2.hashCode()); // false(哈希碼不同,違反約束)
// 放入HashSet
Set<BadPerson> set = new HashSet<>();
set.add(p1);
set.add(p2);
System.out.println(set.size()); // 2(錯(cuò)誤!因?yàn)閜1和p2應(yīng)該被視為相同元素)
}
}原因:HashSet判斷元素是否重復(fù)時(shí),先通過hashCode定位桶位,再用equals比較。由于p1和p2哈希碼不同,會(huì)被放入不同桶位,equals即使返回true也不會(huì)被視為重復(fù)元素。
五、常見誤區(qū)與最佳實(shí)踐
了解了三者的原理后,我們需要規(guī)避一些常見錯(cuò)誤,掌握實(shí)際開發(fā)中的最佳實(shí)踐。
1. 誤區(qū)1:用==比較字符串內(nèi)容
很多初學(xué)者會(huì)犯這樣的錯(cuò)誤:
String s1 = "hello";
String s2 = new String("hello");
if (s1 == s2) { ... } // 錯(cuò)誤!此處比較的是地址,而非內(nèi)容正確做法:字符串內(nèi)容比較必須用equals:
if (s1.equals(s2)) { ... } // 正確,比較內(nèi)容
// 避免空指針異常的寫法(當(dāng)s1可能為null時(shí))
if (Objects.equals(s1, s2)) { ... }2. 誤區(qū)2:重寫equals時(shí)不重寫hashCode
如前文反例所示,這會(huì)導(dǎo)致哈希表(HashMap、HashSet等)工作異常。牢記:重寫equals必須同時(shí)重寫hashCode,且兩者基于相同的屬性計(jì)算。
3. 誤區(qū)3:認(rèn)為hashCode相等的對(duì)象一定相等
哈希碼相等只是"可能相等",而非"一定相等"。例如:
// 兩個(gè)不同的字符串,可能有相同的哈希碼(哈希沖突) String str1 = "Aa"; String str2 = "BB"; System.out.println(str1.hashCode()); // 2112 System.out.println(str2.hashCode()); // 2112 System.out.println(str1.equals(str2)); // false
因此,在哈希表中,hashCode僅用于初步篩選,最終必須通過equals確認(rèn)是否相等。
4. 最佳實(shí)踐總結(jié)
場(chǎng)景 | 正確做法 | 錯(cuò)誤做法 |
比較基本類型值 | 使用== | 試圖用equals(基本類型沒有該方法) |
比較引用類型內(nèi)容 | 使用equals(需確保已重寫) | 使用==(比較地址而非內(nèi)容) |
重寫equals | 同時(shí)重寫hashCode,基于相同屬性計(jì)算 | 只重寫equals,忽略hashCode |
避免equals空指針異常 | 使用Objects.equals(a, b) | 直接調(diào)用a.equals(b)(當(dāng)a可能為null時(shí)) |
判斷對(duì)象是否為同一實(shí)例 | 使用== | 使用equals(可能被重寫,結(jié)果不可靠) |
六、總結(jié)
==、equals和hashCode是Java對(duì)象比較的三大核心工具,它們的設(shè)計(jì)體現(xiàn)了Java對(duì)"身份"與"內(nèi)容"的嚴(yán)格區(qū)分,以及哈希表高效運(yùn)作的底層邏輯:
==:基本類型比"值",引用類型比"地址";equals:默認(rèn)比"地址",重寫后可比"內(nèi)容",需遵循等價(jià)關(guān)系;hashCode:對(duì)象的"數(shù)字指紋",輔助哈希表定位,必須與equals保持一致。
到此這篇關(guān)于Java中的==、equals與hashCode區(qū)別與聯(lián)系最佳實(shí)踐的文章就介紹到這了,更多相關(guān)java == equals hashCode內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
Java數(shù)據(jù)結(jié)構(gòu)與算法之循環(huán)隊(duì)列的實(shí)現(xiàn)
SpringMVC多個(gè)文件上傳及上傳后立即顯示圖片功能
Spring Security UserDetails實(shí)現(xiàn)原理詳解
Java實(shí)現(xiàn)簡(jiǎn)單的RPC框架的示例代碼

