別及底層原理)
記得我第一次面試Java崗位面試官問(wèn)出這道題時(shí)我內(nèi)心是竊喜的這題太熟了張口就能背。可當(dāng)他把代碼題寫(xiě)到白板上讓我判斷每一行輸出的時(shí)候我發(fā)現(xiàn)自己只背會(huì)了一半。再后來(lái)我參與過(guò)一些技術(shù)面試見(jiàn)過(guò)各種各樣的回答才真正搞清楚這道看似基礎(chǔ)的題目背后面試官到底想驗(yàn)證什么。這篇不打算把答案從其他資料里抄一遍而是把這個(gè)問(wèn)題連帶的底層知識(shí)一起拆開(kāi)讓你既能應(yīng)付面試也能在寫(xiě)代碼時(shí)真的用得上。先給個(gè)結(jié)論式的提醒 在比較引用類型時(shí)比較的是兩個(gè)引用是否指向同一個(gè)對(duì)象equals 沒(méi)被重寫(xiě)時(shí)行為跟 完全一致一旦某個(gè)類重寫(xiě)了 equals它比較的才是業(yè)務(wù)意義上的相等。真正把大多數(shù)面試者篩掉的不是這兩句結(jié)論而是為什么。1. 這道題背后的考點(diǎn)地圖面試官真正想確認(rèn)什么1.1 一個(gè)讓我印象深刻的面試回答有次面試候選人我讓他解釋 和 equals 的區(qū)別他非常流利地說(shuō) 是地址比較equals 是內(nèi)容比較所以字符串要用 equals 判斷相等。 我接著問(wèn)了一句那 Integer a 128; Integer b 128; a b 輸出什么 他愣了幾秒小聲說(shuō)應(yīng)該是 false 吧…… 再問(wèn)為什么 127 是 true 而 128 是 false 他就只能搖頭了。這個(gè)場(chǎng)景我見(jiàn)過(guò)太多次。不是候選人沒(méi)背題而是他背的答案只覆蓋了 String 這一個(gè)例子沒(méi)有形成知識(shí)體系。一旦問(wèn)題從 String 換到 Integer、從常量賦值換成 new 對(duì)象、從 equals 換到 hashCode原來(lái)的知識(shí)框架就塌了。1.2 三個(gè)層次決定回答的上限如果給這道題的回答分層次大概是這樣的層次典型表現(xiàn)面試評(píng)價(jià)背結(jié)論只記得比地址equals比內(nèi)容及格線以下懂原理能結(jié)合 JVM 內(nèi)存模型解釋 能講清 Object.equals 默認(rèn)實(shí)現(xiàn)中等偏上能應(yīng)用能解釋 Integer 緩存、String 常量池、hashCode 契約并能答出項(xiàng)目里的實(shí)際比較策略穩(wěn)過(guò)面試問(wèn)基礎(chǔ)題從來(lái)不是考察你是否背過(guò)課本原文而是考察你能不能用一個(gè)基礎(chǔ)問(wèn)題引出整條知識(shí)鏈。所以下面的內(nèi)容順序也是按這條知識(shí)鏈設(shè)計(jì)的先搞懂 在 JVM 層面的真實(shí)語(yǔ)義再搞清楚 equals 被誰(shuí)重寫(xiě)、為什么重寫(xiě)最后把面試最常追問(wèn)的 String、Integer、hashCode、集合比較統(tǒng)一串起來(lái)。2. 先用 JVM 內(nèi)存模型看透 它比的從來(lái)都是棧上的值2.1 引用是什么用儲(chǔ)物柜和倉(cāng)庫(kù)來(lái)理解Java 的內(nèi)存模型對(duì)新手不算友好但 這個(gè)問(wèn)題必須從這里切入。簡(jiǎn)單說(shuō)我們創(chuàng)建一個(gè)對(duì)象時(shí)對(duì)象本體在堆內(nèi)存中而方法里的局部變量存儲(chǔ)在棧上棧上存的不是對(duì)象內(nèi)容而是指向堆中對(duì)象的一個(gè)引用你可以把棧想象成一排儲(chǔ)物柜柜子里放著一張便利貼便利貼上寫(xiě)著倉(cāng)庫(kù)里的貨架編號(hào)倉(cāng)庫(kù)是堆內(nèi)存貨架編號(hào)就是引用值。執(zhí)行User user new User()時(shí)虛擬機(jī)先在堆里分配一塊內(nèi)存放 User 對(duì)象然后把這塊內(nèi)存的地址作為引用值存到棧上的變量user里。所以對(duì)引用類型執(zhí)行本質(zhì)上是比較兩個(gè)儲(chǔ)物柜里的便利貼是不是寫(xiě)著同一個(gè)貨架編號(hào)也就是堆內(nèi)存地址是否相同。還有一個(gè)很重要的點(diǎn)是基本類型不走這套流程。int a 10; int b 10;中棧上直接保存數(shù)值本身不涉及堆對(duì)象a b比較的就是兩個(gè)數(shù)值是否相等。這一點(diǎn)和 equals 毫無(wú)關(guān)系因?yàn)榛绢愋蜎](méi)有方法不能用點(diǎn)運(yùn)算符調(diào)用 equals。2.2 一張表讀懂基本類型和引用類型的 差異看下面這段代碼int x 100; int y 100; System.out.println(x y); // true數(shù)值比較 String s1 new String(abc); String s2 new String(abc); System.out.println(s1 s2); // false兩個(gè)堆對(duì)象引用地址不同x 和 y 在棧上都是數(shù)值 100所以相等s1 和 s2 分別 new 了兩個(gè)對(duì)象即使內(nèi)容完全一樣它們?cè)诙牙锸莾蓧K不同的內(nèi)存棧上的引用地址自然不同返回 false。這是第一步也是整個(gè)問(wèn)題的地基永遠(yuǎn)比較棧上的值。對(duì)基本類型來(lái)說(shuō)這個(gè)值就是數(shù)據(jù)本身對(duì)引用類型來(lái)說(shuō)這個(gè)值是堆內(nèi)存地址。記住這句后面所有場(chǎng)景都能推導(dǎo)出來(lái)。3. equals 的本來(lái)面目Object 默認(rèn)實(shí)現(xiàn)就是 重寫(xiě)它的人改了什么3.1 Object.equals 的源碼真相很多人以為 equals 天然就是比較內(nèi)容這是個(gè)危險(xiǎn)的誤解??匆幌?Object 類的源碼public boolean equals(Object obj) { return (this obj); }你沒(méi)有看錯(cuò)默認(rèn)的 equals 內(nèi)部就是用判斷的。Object 是所有類的父類它根本不知道子類有哪些字段所以只能力所能及地比較引用地址。只有當(dāng)某個(gè)類重寫(xiě)了 equals我們才能獲得業(yè)務(wù)上的相等語(yǔ)義。換句話說(shuō)面試時(shí)如果有人問(wèn)equals 是不是就是比較內(nèi)容標(biāo)準(zhǔn)答案是默認(rèn) Object.equals 比較的是引用只有重寫(xiě)后的 equals 才有可能是內(nèi)容比較。這也是為什么題目問(wèn)的是 和 equals 有什么區(qū)別而不是 比較地址equals 比較內(nèi)容這么簡(jiǎn)單的原因。3.2 哪些類重寫(xiě)了 equals為什么在標(biāo)準(zhǔn)類庫(kù)里以下常見(jiàn)類都沒(méi)有繼承默認(rèn)的 Object.equals而是根據(jù)自己的業(yè)務(wù)語(yǔ)義重寫(xiě)了類equals 的語(yǔ)義典型用途String逐字符比較字符串內(nèi)容判斷兩個(gè)字符串是否長(zhǎng)得一樣Integer 等包裝類比較包裝的數(shù)值數(shù)值比較Long比較數(shù)值但注意派生自 Number 時(shí)略有差異數(shù)值比較BigDecimal比較數(shù)值與精度 scale金額計(jì)算Date比較時(shí)間戳?xí)r間判斷集合類List/Map/Set比較元素是否完全相同集合相等判斷自定義實(shí)體類由開(kāi)發(fā)者自己決定哪些字段參與比較業(yè)務(wù)主鍵判斷這些類的共同點(diǎn)是它們都有值語(yǔ)義。String 的值是字符串內(nèi)容Integer 的值是int 數(shù)值一個(gè) ArrayList 的值是內(nèi)部元素列表。既然它們代表的是值就得按值來(lái)判斷相等而不是按內(nèi)存地址。這個(gè)設(shè)計(jì)動(dòng)機(jī)如果你記住了面試官問(wèn)為什么 Integer 要重寫(xiě) equals你就不會(huì)被問(wèn)倒。3.3 Java 規(guī)范對(duì) equals 重寫(xiě)的五條硬性契約任何類重寫(xiě) equals都要遵守 Java 官方規(guī)定的五條規(guī)則違反任何一條都會(huì)導(dǎo)致集合類、依賴 equals 的框架出現(xiàn)詭異問(wèn)題自反性x.equals(x)必須返回 true集合里不會(huì)出現(xiàn)自己不等于自己的怪事對(duì)稱性x.equals(y)為 true 時(shí)y.equals(x)也必須為 true傳遞性x.equals(y)和y.equals(z)都為 true 時(shí)x.equals(z)必須為 true一致性在對(duì)象沒(méi)有被修改的前提下多次調(diào)用 equals 結(jié)果必須一致不能今天 true 明天 false非空性x.equals(null)必須返回 false不能拋空指針。對(duì)稱性這條尤其容易在繼承場(chǎng)景踩雷父類用instanceof判斷類型子類也同樣的寫(xiě)法就很可能出現(xiàn)parent.equals(child)為 true、child.equals(parent)也為 true但一旦兩邊 hashCode 不一致HashMap 直接翻車。我見(jiàn)過(guò)有同事在 equals 里偷懶只比較 id但 id 用比較 Long導(dǎo)致兩個(gè) id 都是 100L 的對(duì)象一個(gè) true 一個(gè) false就屬于違反一致性。這些坑后面會(huì)展開(kāi)。4. 把 String 單獨(dú)拎出來(lái)講字符串池、new String 與 intern 的連環(huán)追問(wèn)4.1 幾乎必考的字符串判斷輸出到底怎么排這道代碼題我面試時(shí)經(jīng)常現(xiàn)場(chǎng)寫(xiě)String s1 abc; String s2 abc; String s3 new String(abc); String s4 s3.intern(); System.out.println(s1 s2); // 輸出 true System.out.println(s1 s3); // 輸出 false System.out.println(s1.equals(s3)); // 輸出 true System.out.println(s1 s4); // 輸出 true逐行看原因。第一行s1和s2都是字面量賦值編譯期就能確定內(nèi)容虛擬機(jī)會(huì)把它們放進(jìn)字符串常量池兩個(gè)變量最終指向常量池里的同一個(gè)對(duì)象所以為 true。字符串常量池的本質(zhì)就是復(fù)用避免相同內(nèi)容的字符串重復(fù)占用內(nèi)存這是 JVM 做出的優(yōu)化設(shè)計(jì)。第二行關(guān)鍵在new String(abc)。abc 這個(gè)字面量先進(jìn)了常量池然后 new 又在堆里新造了一個(gè) String 對(duì)象。s3指向的是堆里那個(gè)新對(duì)象而s1指向的是常量池里的老對(duì)象二者地址顯然不同所以為 false。注意一個(gè)細(xì)節(jié)即使堆里已經(jīng)有一個(gè)內(nèi)容為 abc 的對(duì)象JVM 也不會(huì)因此取消 new 的執(zhí)行new 的語(yǔ)義就是創(chuàng)建一個(gè)新對(duì)象。第三行就是 String 重寫(xiě) equals 的價(jià)值雖然內(nèi)存地址不同但逐字符比較下來(lái)內(nèi)容一致所以返回 true。這也是實(shí)際開(kāi)發(fā)中判斷字符串是否相等、永遠(yuǎn)不要用的原因。第四行涉及intern()方法它把字符串內(nèi)容放到常量池中如果常量池已存在相同內(nèi)容就直接返回那個(gè)引用。所以s4拿到的正是s1指向的那個(gè)對(duì)象為 true。JDK7 以后常量池就移到了堆內(nèi)存中這里面的細(xì)節(jié)很多但面試能答到intern 會(huì)把字符串引用轉(zhuǎn)存到常量池基本就足夠了。4.2 字符串拼接的另一種坑還有一個(gè)高頻變形題String a hello; String b world; String c hello world; // 編譯期常量折疊 String d a b; // 運(yùn)行期拼接 System.out.println(c helloworld); // truec 在編譯期已經(jīng)確定 System.out.println(d helloworld); // falsed 在運(yùn)行期通過(guò) StringBuilder 拼接hello world兩邊的都是常量編譯期 javac 會(huì)直接計(jì)算出結(jié)果 helloworld所以 c 指向常量池里的對(duì)象。而a b兩個(gè)變量在編譯期無(wú)法確定內(nèi)容運(yùn)行時(shí)會(huì)通過(guò)StringBuilder.append拼出新的 String 對(duì)象d 指向堆內(nèi)存新對(duì)象自然不等于常量池里的字面量。面試進(jìn)行到這里基本上已經(jīng)把 String、常量池、引用比較全部覆蓋了。如果候選人能順著這個(gè)思路自己推出所以字符串相等判斷應(yīng)該用 equals面試官對(duì)基礎(chǔ)能力的信任度會(huì)高很多。5. 重寫(xiě) equals 的正確姿勢(shì)以及 hashCode 為何必須一起重寫(xiě)5.1 一個(gè)規(guī)范的業(yè)務(wù)實(shí)體 equals 模板重寫(xiě) equals 不是隨便把字段拿來(lái)一遍尤其是含有 Long、String 這種引用類型字段時(shí)直接用比較字段值等于沒(méi)比較。下面是一個(gè)比較標(biāo)準(zhǔn)的寫(xiě)法public class User { private Long id; private String name; private int age; Override public boolean equals(Object o) { if (this o) return true; // 同一對(duì)象直接 true if (o null || getClass() ! o.getClass()) return false; // 類型嚴(yán)格相同 User user (User) o; return age user.age // 基本類型用 Objects.equals(id, user.id) // Long 用 Objects.equals空安全 Objects.equals(name, user.name); // String 用 Objects.equals } Override public int hashCode() { return Objects.hash(id, name, age); } }這里說(shuō)明幾個(gè)關(guān)鍵點(diǎn)。先判斷this o是同一個(gè)對(duì)象直接返回 true省去后面的字段比較性能最好。再判斷o null滿足非空性契約。類型判斷這里用getClass() ! o.getClass()嚴(yán)格要求必須是同一個(gè)類避免父類子類混比。比較字段時(shí)基本類型直接用引用類型調(diào)用Objects.equals這個(gè)靜態(tài)工具類內(nèi)部做了空值判斷兩個(gè)都為 null 時(shí)返回 true避免手寫(xiě)空指針判斷。Objects.equals的源碼邏輯是這樣的return (a b) || (a ! null a.equals(b));入?yún)蓚€(gè)都 null 時(shí)第一個(gè)條件已經(jīng)成立一個(gè)為 null 時(shí)第二個(gè)條件里的a ! null直接屏蔽掉 equals 調(diào)用所以永遠(yuǎn)不會(huì)空指針。寫(xiě)業(yè)務(wù) equals 時(shí)直接用它就夠了。5.2 如果不重寫(xiě) hashCodeHashMap 現(xiàn)場(chǎng)翻車重寫(xiě) equals 必須同時(shí)重寫(xiě) hashCode這條契約入職第一天就該刻在腦子里。原因要用 HashMap 的存和取來(lái)說(shuō)明。假設(shè)我定義了一個(gè)PhoneNumber類只有 areaCode 和 number 兩個(gè)字段只重寫(xiě)了 equals 沒(méi)重寫(xiě) hashCodeMapPhoneNumber, String map new HashMap(); map.put(new PhoneNumber(020, 12345678), 張三); String name map.get(new PhoneNumber(020, 12345678)); System.out.println(name); // 輸出 null找不到put 的時(shí)候HashMap 先用 hash 算法算出 key 的 hashCode把 entry 放進(jìn)某個(gè)桶里get 的時(shí)候它先根據(jù)傳入 key 的 hashCode 定位到同一個(gè)桶然后在桶里用 equals 逐個(gè)比對(duì)。兩個(gè)對(duì)象雖然 equals 相等但 hashCode 不同put 和 get 分別定位到了不同的桶equals 根本沒(méi)機(jī)會(huì)被調(diào)到。這個(gè)案例能直觀說(shuō)明 hashCode 契約的意義equals 相等的兩個(gè)對(duì)象hashCode 必須相等否則基于 hash 的集合統(tǒng)統(tǒng)失效。反過(guò)來(lái) hashCode 相同的兩個(gè)對(duì)象equals 不一定要相等因?yàn)?hash 碰撞本身允許發(fā)生碰撞時(shí)再用 equals 區(qū)辨。這也是為什么 HashMap 允許不同 key 落在同一桶里。5.3 類型判斷instanceof 和 getClass 該怎么選寫(xiě) equals 時(shí)一個(gè)容易被追問(wèn)的細(xì)節(jié)是類型判斷用instanceof還是getClass()。getClass()嚴(yán)格要求兩個(gè)對(duì)象運(yùn)行時(shí)類型完全一致比如 User.class 只能和 User 比較子類對(duì)象不會(huì)被認(rèn)為是相等的。這種方案安全但偏保守一旦父類有子類父類的 equals 永遠(yuǎn)無(wú)法把子類視為相等業(yè)務(wù)上想比較父子類型同 id 也算相等就很難做到。instanceof允許子類對(duì)象在父類的 equals 里通過(guò)類型檢查配合向下轉(zhuǎn)型比較字段能實(shí)現(xiàn)更靈活的比較。但它有個(gè)著名副作用破壞對(duì)稱性。例如父類用instanceof允許比較子類子類也重寫(xiě)了 equals 的話可能會(huì)出現(xiàn)parent.equals(child)為 true 而child.equals(parent)為 false 的不對(duì)稱情況。Effective Java 處理這個(gè)問(wèn)題建議配合 canEqual 模式很多框架的內(nèi)部類就這么做。我個(gè)人的實(shí)踐傾向是業(yè)務(wù)實(shí)體類通常不會(huì)主動(dòng)設(shè)計(jì)繼承體系直接用getClass() ! o.getClass()最省事也不會(huì)出幺蛾子如果確實(shí)要支持子類擴(kuò)展再考慮用instanceof配合子類重設(shè) canEqual。面試時(shí)能說(shuō)出這兩種方案的取舍是加分項(xiàng)。6. 真實(shí)業(yè)務(wù)里那些 造成的踩坑實(shí)錄6.1 Integer 緩存127 和 128 的驚魂一刻這是我見(jiàn)過(guò)新人最容易摔跤的地方Integer a 127; Integer b 127; System.out.println(a b); // true Integer c 128; Integer d 128; System.out.println(c d); // false同樣是自動(dòng)裝箱為什么結(jié)果不一樣因?yàn)?jdk 在 Integer 類里自帶一個(gè)緩存池IntegerCache默認(rèn)緩存了 -128 到 127 之間的對(duì)象。當(dāng)代碼里出現(xiàn)Integer a 127時(shí)自動(dòng)裝箱會(huì)調(diào)用Integer.valueOf(127)這個(gè)方法在緩存范圍內(nèi)直接返回緩存池里的同一個(gè)對(duì)象所以a b命中同一個(gè)引用。到了 128超出緩存范圍valueOf 就會(huì) new 一個(gè)新對(duì)象返回c 和 d 各 new 各的自然為 false。這個(gè)緩存的邊界可以通過(guò)-XX:AutoBoxCacheMax參數(shù)調(diào)整但實(shí)際開(kāi)發(fā)中我不會(huì)依賴它做判斷。唯一的正確姿勢(shì)是包裝類之間比較Integer、Long、Short、Byte、Character、Boolean一律用equals或Objects.equals不要賭緩存范圍。Long 也有同樣的緩存機(jī)制范圍同樣是 -128 到 127。6.2 ArrayList.contains 和 HashSet 去重其實(shí)是 equals 在替你干活真實(shí)業(yè)務(wù)里經(jīng)常要判斷列表是否包含某個(gè)元素ListString list Arrays.asList(a, b); System.out.println(list.contains(a)); // trueString 重寫(xiě)了 equals ListUser users new ArrayList(); users.add(new User(1L, 張三)); System.out.println(users.contains(new User(1L, 張三))); // 如果 User 沒(méi)有重寫(xiě) equals輸出 false重寫(xiě)了就輸出 trueArrayList 的 contains 邏輯很直接遍歷內(nèi)部數(shù)組對(duì)每個(gè)元素調(diào)用元素.equals(入?yún)?逐個(gè)比對(duì)找到就返回 true。如果 User 不重寫(xiě) equals默認(rèn)走 Object.equals比較的是引用地址新 new 出來(lái)的 User 和已放進(jìn)去的 User 不是同一個(gè)對(duì)象所以 contains 永遠(yuǎn)返回 false。很多人開(kāi)發(fā)時(shí)遇到明明數(shù)據(jù)看起來(lái)一樣contains 卻返回 false多半就是這個(gè)原因。HashSet 去重也是同理。HashSet 底層是 HashMap往里面 add 元素等于把元素作為 key 插入 map去重依賴的就是 hashCode 和 equals。兩個(gè)內(nèi)容相同的對(duì)象如果 hashCode 不一樣HashSet 會(huì)把它們當(dāng)成兩個(gè) key去重形同虛設(shè)。這也是為什么寫(xiě)了 equals 就必須看一眼 hashCode 的典型場(chǎng)景。6.3 BigDecimal 金額比較equals 比的是數(shù)值還是精度金額計(jì)算里 BigDecimal 是主角但它的 equals 有個(gè)大坑BigDecimal m1 new BigDecimal(0.0); BigDecimal m2 new BigDecimal(0); System.out.println(m1.equals(m2)); // false數(shù)值都是 0但 scale 不同 System.out.println(m1.compareTo(m2)); // 0compareTo 才真正相等BigDecimal 重寫(xiě) equals 時(shí)不僅比較數(shù)值還比較精度 scale。0.0的 scale 是 10的 scale 是 0所以 equals 返回 false。但是業(yè)務(wù)上判斷金額是否相等通常只關(guān)心數(shù)值是否一樣用 equals 就會(huì)出問(wèn)題。金額比較的正確姿勢(shì)是用compareTo它忽略尾部多余的零只看數(shù)值。類似的還有 Long 與 Integer 的 mix 比較、Double 與 Float 的比較都是看起來(lái)一樣equals 卻 false的家族成員。業(yè)務(wù)里判斷數(shù)據(jù)庫(kù)主鍵或狀態(tài)值時(shí)我的習(xí)慣是統(tǒng)一走Objects.equals(attr1, attr2)而不是attr1 attr2。因?yàn)?attr 一旦從方法返回很可能是包裝類型在超范圍場(chǎng)景下會(huì)給出錯(cuò)誤結(jié)果用 Objects.equals 既空安全又避免掉進(jìn)緩存區(qū)間的陷阱。7. 面試官的高頻追問(wèn)鏈與一套能壓住場(chǎng)的回答話術(shù)7.1 常見(jiàn)追問(wèn)鏈從這道題可以延伸到哪里面試官通常不會(huì)只滿足于標(biāo)準(zhǔn)答案他會(huì)順著你的回答不斷深挖問(wèn)equals 和 hashCode 有什么關(guān)系問(wèn)為什么重寫(xiě) equals 必須重寫(xiě) hashCode不重寫(xiě)會(huì)怎樣問(wèn)String 的 equals 是怎么實(shí)現(xiàn)的 在字符串上什么時(shí)候相等問(wèn)Integer 的 什么時(shí)候返回 true為什么 127 和 128 結(jié)果不同問(wèn)兩個(gè)對(duì)象 equals 相等hashCode 一定相等嗎反過(guò)來(lái)呢問(wèn)HashMap 的 get 過(guò)程中hashCode 和 equals 分別起了什么作用問(wèn)對(duì)象放入 HashSet什么時(shí)候能去重什么時(shí)候不能問(wèn)你的實(shí)體類重寫(xiě) equals 了嗎用什么比較主鍵這條追問(wèn)鏈非常經(jīng)典本質(zhì)上是想看你能否把一個(gè)知識(shí)點(diǎn)串成體系同時(shí)觀察你平時(shí)寫(xiě)代碼時(shí)有沒(méi)有思考過(guò)集合類的工作原理。所以準(zhǔn)備這道題時(shí)不建議只背標(biāo)準(zhǔn)答案建議把 HashMap 的存取流程、Integer 緩存機(jī)制、String 常量池這幾塊都過(guò)一遍。7.2 一套參考回答話術(shù)如果面試官讓你用 1 到 2 分鐘回答 和 equals 的區(qū)別可以這樣說(shuō)先分對(duì)象類型。對(duì)于基本類型 比較的是數(shù)值對(duì)于引用類型 比較的是兩個(gè)引用是否指向堆里的同一個(gè)對(duì)象也就是比較內(nèi)存地址。equals 是 Object 類的方法默認(rèn)實(shí)現(xiàn)其實(shí)就是return this obj所以沒(méi)有重寫(xiě)時(shí)equals 和 效果完全一樣。String、Integer、BigDecimal 這些類為了值語(yǔ)義都重寫(xiě)了 equals改成了根據(jù)內(nèi)容判斷相等。在業(yè)務(wù)代碼里判斷字符串相等用 equals判斷包裝類數(shù)值相等用 equals 或 Objects.equals判斷兩個(gè)對(duì)象是否相等則依賴實(shí)體類是否重寫(xiě)了 equals 和 hashCode。并且重寫(xiě) equals 必須同時(shí)重寫(xiě) hashCode否則 HashMap、HashSet 這類散列集合在存和取時(shí)可能定位到不同桶導(dǎo)致 equals 相等卻取不到數(shù)據(jù)。這段話的好處是先給分類結(jié)論再落到 Object 默認(rèn)實(shí)現(xiàn)接著舉標(biāo)準(zhǔn)庫(kù)例子最后引出 hashCode 契約和集合類的關(guān)系。面試官順著任意一條往下問(wèn)你都有內(nèi)容接得上。最后分享一個(gè)我自己的實(shí)踐習(xí)慣給實(shí)體類寫(xiě) equals 之前先問(wèn)自己幾個(gè)問(wèn)題——這個(gè)類會(huì)不會(huì)放進(jìn) Set 或作為 Map 的 key會(huì)不會(huì)用來(lái)做 List.contains 判斷如果會(huì)就老老實(shí)實(shí)地把所有業(yè)務(wù)字段都納入比較并配上 hashCode如果只在單一方法內(nèi)做局部比較用 Objects.equals 判斷幾個(gè)字段就夠了不必為整個(gè)類承擔(dān)重寫(xiě) equals 的負(fù)擔(dān)。很多線上問(wèn)題并不復(fù)雜翻來(lái)覆去就是這幾個(gè)比較語(yǔ)義沒(méi)想清楚。把這套東西弄明白這道面試題和它背后的知識(shí)鏈基本就穩(wěn)了。