equals 和 == 之戰(zhàn)
“哥,如何比較兩個字符串相等啊?”三妹問。
“這個問題看似簡單,卻在 Stack Overflow 上有超過 370 萬的訪問量?!蔽艺f,“這個問題也可以引申為 .equals() 和 ‘==’ 操作符有什么區(qū)別。”
“==”操作符用于比較兩個對象的地址是否相等。 .equals()方法用于比較兩個對象的內(nèi)容是否相等。
“不是很理解?!比酶械胶芾Щ?。
“我來舉個不恰當(dāng)又很恰當(dāng)?shù)睦?,一看你就明白了,三妹。?/p>
有一對雙胞胎,姐姐叫阿麗塔,妹妹叫洛麗塔。我們普通人可能完全無法分辨誰是姐姐誰是妹妹,可她們的媽媽卻可以輕而易舉地辨認出。

.equals() 就好像我們普通人,看見阿麗塔以為是洛麗塔,看見洛麗塔以為是阿麗塔,看起來一樣就覺得她們是同一個人;“==”操作符就好像她們的媽媽,要求更嚴格,觀察更細致,一眼就能分辨出誰是姐姐誰是妹妹。
String alita = new String("小蘿莉");
String luolita = new String("小蘿莉");
System.out.println(alita.equals(luolita)); // true
System.out.println(alita == luolita); // false
就上面這段代碼來說,.equals() 輸出的結(jié)果為 true,而“==”操作符輸出的結(jié)果為 false——前者要求內(nèi)容相等就可以,后者要求必須是同一個對象。
“三妹,之前已經(jīng)學(xué)過了,Java 的所有類都默認地繼承 Object 這個超類,該類有一個名為 .equals() 的方法。”一邊說,我一邊打開了 Object 類的源碼。
public boolean equals(Object obj) {
return (this == obj);
}
你看,Object 類的 .equals() 方法默認采用的是“==”操作符進行比較。假如子類沒有重寫該方法的話,那么“==”操作符和 .equals() 方法的功效就完全一樣——比較兩個對象的內(nèi)存地址是否相等。
但實際情況中,有不少類重寫了 .equals() 方法,因為比較內(nèi)存地址的要求比較嚴格,不太符合現(xiàn)實中所有的場景需求。拿 String 類來說,我們在比較字符串的時候,的確只想判斷它們倆的內(nèi)容是相等的就可以了,并不想比較它們倆是不是同一個對象。
況且,字符串有字符串常量池的概念,本身就推薦使用 String s = "字符串" 這種形式來創(chuàng)建字符串對象,而不是通過 new 關(guān)鍵字的方式,因為可以把字符串緩存在字符串常量池中,方便下次使用。
“哦,我明白了。”三妹說。
“那就來看一下 .equals() 方法的源碼吧?!蔽艺f。
public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
if (anObject instanceof String) {
String aString = (String)anObject;
if (coder() == aString.coder()) {
return isLatin1() ? StringLatin1.equals(value, aString.value)
: StringUTF16.equals(value, aString.value);
}
}
return false;
}
首先,如果兩個字符串對象的可以“==”,那就直接返回 true 了,因為這種情況下,字符串內(nèi)容是必然相等的。否則就按照字符編碼進行比較,分為 UTF16 和 Latin1,差別不是很大,就拿 Latin1 的來說吧。
@HotSpotIntrinsicCandidate
public static boolean equals(byte[] value, byte[] other) {
if (value.length == other.length) {
for (int i = 0; i < value.length; i++) {
if (value[i] != other[i]) {
return false;
}
}
return true;
}
return false;
}
我的 JDK 版本是 Java 11,也就是最新的 LTS(長期支持)版本。該版本中,String 類使用字節(jié)數(shù)組實現(xiàn)的,所以比較兩個字符串的內(nèi)容是否相等時,可以先比較字節(jié)數(shù)組的長度是否相等,不相等就直接返回 false;否則就遍歷兩個字符串的字節(jié)數(shù)組,只有有一個字節(jié)不相等,就返回 false。
“嗯,二哥,這段源碼不難理解?!比米孕诺卣f。
“那出幾道題考考你吧!”我說。
第一題:
new String("小蘿莉").equals("小蘿莉")
“輸出什么呢?”我問。
“.equals() 比較的是兩個字符串對象的內(nèi)容是否相等,所以結(jié)果為 true?!比么?。
第二題:
new String("小蘿莉") == "小蘿莉"
“==操作符左側(cè)的是在堆中創(chuàng)建的對象,右側(cè)是在字符串常量池中的對象,盡管內(nèi)容相同,但內(nèi)存地址不同,所以返回 false?!比么?。
第三題:
new String("小蘿莉") == new String("小蘿莉")
“new 出來的對象肯定是完全不同的內(nèi)存地址,所以返回 false?!比么?。
第四題:
"小蘿莉" == "小蘿莉"
“字符串常量池中只會有一個相同內(nèi)容的對象,所以返回 true?!比么?。
第五題:
"小蘿莉" == "小" + "蘿莉"
“由于‘小’和‘蘿莉’都在字符串常量池,所以編譯器在遇到‘+’操作符的時候?qū)⑵渥詣觾?yōu)化為“小蘿莉”,所以返回 true?!?/p>
第六題:
new String("小蘿莉").intern() == "小蘿莉"
“new String("小蘿莉") 在執(zhí)行的時候,會先在字符串常量池中創(chuàng)建對象,然后再在堆中創(chuàng)建對象;執(zhí)行 intern() 方法的時候發(fā)現(xiàn)字符串常量池中已經(jīng)有了‘小蘿莉’這個對象,所以就直接返回字符串常量池中的對象引用了,那再與字符串常量池中的‘小蘿莉’比較,當(dāng)然會返回 true 了?!比谜f。
哇,不得不說,三妹前幾節(jié)的字符串相關(guān)內(nèi)容都完全學(xué)會了呀!
“三妹,哥再給你補充一點?!蔽艺f。
“如果要進行兩個字符串對象的內(nèi)容比較,除了 .equals() 方法,還有其他兩個可選的方案?!?/p>
1)Objects.equals()
Objects.equals() 這個靜態(tài)方法的優(yōu)勢在于不需要在調(diào)用之前判空。
public static boolean equals(Object a, Object b) {
return (a == b) || (a != null && a.equals(b));
}
如果直接使用 a.equals(b),則需要在調(diào)用之前對 a 進行判空,否則可能會拋出空指針 java.lang.NullPointerException。Objects.equals() 用起來就完全沒有這個擔(dān)心。
Objects.equals("小蘿莉", new String("小" + "蘿莉")) // --> true
Objects.equals(null, new String("小" + "蘿莉")); // --> false
Objects.equals(null, null) // --> true
String a = null;
a.equals(new String("小" + "蘿莉")); // throw exception
2)String 類的 .contentEquals()
.contentEquals() 的優(yōu)勢在于可以將字符串與任何的字符序列(StringBuffer、StringBuilder、String、CharSequence)進行比較。
public boolean contentEquals(CharSequence cs) {
// Argument is a StringBuffer, StringBuilder
if (cs instanceof AbstractStringBuilder) {
if (cs instanceof StringBuffer) {
synchronized(cs) {
return nonSyncContentEquals((AbstractStringBuilder)cs);
}
} else {
return nonSyncContentEquals((AbstractStringBuilder)cs);
}
}
// Argument is a String
if (cs instanceof String) {
return equals(cs);
}
// Argument is a generic CharSequence
int n = cs.length();
if (n != length()) {
return false;
}
byte[] val = this.value;
if (isLatin1()) {
for (int i = 0; i < n; i++) {
if ((val[i] & 0xff) != cs.charAt(i)) {
return false;
}
}
} else {
if (!StringUTF16.contentEquals(val, cs, n)) {
return false;
}
}
return true;
}
從源碼上可以看得出,如果 cs 是 StringBuffer,該方法還會進行同步,非常的智能化;如果是 String 的話,其實調(diào)用的還是 equals() 方法。當(dāng)然了,這也就意味著使用該方法進行比較的時候,多出來了很多步驟,性能上有些損失。
“是的,總體上感覺還是 Objects.equals() 比較舒服?!比玫难劬κ茄┝恋?,發(fā)現(xiàn)了這個方法的優(yōu)點。
---未完待續(xù),期待下集---
點擊「閱讀原文」可直達《教妹學(xué)Java》專欄的在線閱讀地址!
