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