ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

==和equals到底有什么区别?从内存模型到源码一次讲透

==和equals到底有什么区别?从内存模型到源码一次讲透 面试过几百个Java候选人之后我有个感受越来越强烈基础题不是看你会不会背而是看你能不能讲透。 和 equals 到底有什么区别这道题几乎出现在每一轮Java基础面试里属于Java面试八股文中人气最高的开场题但能答到点子上的人少得可怜。大部分人张口就是比较的是地址equals比较的是内容可当我继续追问两个内容相同的String用比较是什么结果、Integer的128为什么用是false、重写equals为什么必须重写hashCode的时候现场经常陷入沉默。所以我今天不打算只给一个结论而是把这道题背后涉及的内存模型、String常量池、包装类缓存、equals与hashCode的契约、容器类查找机制全部串起来。无论你是准备Java校招、社招还是工作中想少踩几个比较相关的坑这篇内容都适合你慢慢看。核心目标就一个让你下次遇到和equals不仅知道结论还能说出原理也能写出不会出错的代码。1. 面试官为什么总爱问这个问题题目背后的知识点地图1.1 一个看似简单的问题牵出了多少底层知识面试官问这道题不是为了让你花30秒把结论背出来而是想用最小代价判断你的Java基础到底扎不扎实。这个题目太会选位置了它处在语法和原理的交界地带。你答得浅就是比较地址equals比较内容一句话你答得深可以一路聊到JVM内存结构、Object类源码、String常量池、Integer缓存、hashCode契约、HashMap的存取机制。所以这道题本质上是一张知识点地图的入口。很多Java面试题库把它放在第一题是有道理的——面试官会根据你的第一反应调整后续提问方向。如果候选人能把这道题讲得层次分明我大概率会接着问HashMap的put流程、重写hashCode的规范、String.intern的细节如果候选人只会背结论那我基本能判断后面再问深一点也会翻车。这不叫刁难而是八股文筛人最常见也最有效的方式。了解这一点之后你再看网上那些和equals区别的背诵版答案就会明白为什么背了也没用。真正有效的准备方式是把结论放回内存模型和源码里理解它为什么是这样。1.2 别急着背结论先理解到底在比什么是一个运算符它做的事情叫值比较。但这里有个关键地方这个值在不同场景下含义完全不一样。如果是基本类型比如int、double、boolean变量里直接存的就是数据本身比较的就是数值大小如果是引用类型变量里存的是一个引用我们可以把它粗略理解为对象在堆内存中的地址比较的就是这两个引用是否指向同一个对象。用生活里的例子类比你手里有两张同一栋房子的照片照片上看起来一模一样但比较的不是拍的是不是同一栋房子而是你俩手里拿的是不是同一张底片。哪怕两张照片内容完全相同只要来源不是同一张底片结果就是false。很多新手在这个地方容易混淆就是因为忽略了引用类型和基本类型的内存存储差异。所以严格来说比较地址这句话只针对引用类型才准确。如果你对面试官说比较的是值也不算全错但要能立刻补充说明基本类型和引用类型各自比较的是什么值这才算真正理解。1.3 equals并不是天生就按内容比较很多初学者的另一个误区是默认equals就代表内容比较。这个印象来自String和Integer这些类用equals比较会出现看起来相等的结果。但只要你去看Object类的源码就会发现一个简单的事实public boolean equals(Object obj) { return (this obj); }没错equals在Object里的默认实现就是调用判断两个引用是否指向同一个对象。也就是说如果你定义了一个自定义类并且没有重写equals那么你调用equals比较两个对象得到的结果和使用没有任何区别。这也是很多人写实体类时踩坑的开始以为equals天然比较内容结果两个字段完全一样的对象放进了HashSet居然重复了从一个Map里get一个相等但不同实例的对象返回null。原因都出在没有重写equalsequals退化为上。这个点往往是面试官最喜欢追问的隐藏题眼。2. 核心原理拆解从内存模型到源码实现2.1 基本类型与引用类型的第一层分水岭Java有8种基本类型byte、short、int、long、float、double、char、boolean。这些基本类型的变量在栈内存中直接保存值所以用比较它们本质上就是比较两个数值是否相等没有任何歧义。int a 100; int b 100; System.out.println(a b); // true char c1 A; char c2 A; System.out.println(c1 c2); // true引用类型变量则不一样。你写的Person p new Person()JVM会在堆内存中创建Person对象然后把对象的地址存入栈中的变量p。此时p这个变量里存的不是人而是一个门牌号。两个new出来的对象即使每个字段都一样也是两个不同的门牌号用比较自然是false。这里有一个小细节值得记住基本类型本身不是对象所以没有equals方法。你不能写int a 10; a.equals(10)编译器直接报错。如果想用equals只能装箱成Integer但日常开发里基本类型比较直接用就够了没必要绕一圈。2.2 引用比较的内存本质栈里的值就是堆里的地址我们用一个最简单的例子来看引用比较的内存本质public class MemoryDemo { public static void main(String[] args) { String a new String(hello); String b new String(hello); System.out.println(a b); // false } }这段代码创建了两个String对象a和b分别指向堆内存中两个不同地址。用比较时JVM比较的是a和b这两个栈变量里存的地址数值而不是去比较它们指向的字符串内容。两个地址不同所以结果是false。你可以打开IDEA的Debug模式在断点处查看a和bIDEA会显示它们有不同的对象id这个id其实就是模拟了它们在JVM内部的身份标识。以后遇到两个对象看起来一样却返回false的情况不要再怀疑Java有什么问题先默认结论栈里的地址不同自然不相等。理解这一点后有一个好处在面试中如果被问到new String(abc)和字面量abc用比较是什么结果你不光能说出false还能解释清楚栈、堆、常量池各自保存了什么。2.3 Object.equals的默认实现它其实就是Object是所有类的父类所以equals方法每个类都有。默认实现上面已经看过源码了就一行return (this obj)。这里有个容易忽略的细节equals这个方法是public且非final的天然为了重写而设计。String、Integer、Long、Double这些包装类型都重写了equals按值或内容来比较。但自定义类如果不重写就会继承Object的默认版本表现和一模一样。很多业务上一个逻辑相等的需求比如两个User对象只要id相同就算同一个人就必须通过重写equals来实现。否则你用user1.equals(user2)比较两个id相同的对象拿到的还是false。另外Java 7推出过一个工具方法Objects.equals(Object a, Object b)它的实现大致是return a b || (a ! null a.equals(b))。好处是可以安全处理null不用先判空再equals。在实际项目中我建议能用Objects.equals就用它能省掉很多空指针麻烦。2.4 String为什么特别常量池与重写后的equalsString之所以在这道题里被反复拿出来当例子是因为它既有字符串常量池的特殊机制又重写了equals。先说常量池Java为了让字符串这种高频对象能被复用专门维护了一个字符串常量池。String s1 hello; String s2 hello; System.out.println(s1 s2); // true当你用字面量hello赋值时JVM会去常量池里找是否存在这个字符串。如果存在直接返回常量池中那个对象的引用如果不存在先在常量池里创建再返回引用。所以s1和s2都指向常量池里的同一个对象用比较就是true。但如果你用newString s3 new String(hello); System.out.println(s1 s3); // falsenew关键字无论如何都会在堆内存中创建一个新对象。即使常量池里已经存在hellos3指向的也是一个新的堆对象所以s1 s3是false。而s1.equals(s3)是true因为String重写了equals按字符序列逐个比较内容。再说一个面试加分点String.intern()方法。调用hello.intern()会尝试把字符串放入常量池如果常量池已经有相同内容就返回常量池中的引用。所以String s3 new String(hello); System.out.println(s1 s3.intern()); // trueJDK7之后字符串常量池被移到了堆中intern的行为比JDK6更好理解。这个点不需要背得太细但能在面试中提到会比单纯背结论强很多。3. 实操演示一组代码看穿和equals的所有表现3.1 测试环境与准备在动手之前先确认你的电脑上JDK环境是好的。不管你是用JDK8还是JDK17这段代码的运行结果都不会变。如果你还没配置好Java环境先去把JDK装上配置好JAVA_HOME环境变量保证命令行里java -version能正常输出版本号再开始测试。我用的是OpenJDK 8在IDEA里创建一个普通Java类类名就叫EqualsDemo。下面的代码把字符串、包装类、自定义类全部测一遍建议你自己照着敲一遍亲眼看到输出结果比看十遍文章都管用。3.2 字符串、包装类、自定义对象逐个试import java.util.Objects; public class EqualsDemo { static class User { String name; int age; User(String name, int age) { this.name name; this.age age; } } public static void main(String[] args) { // 1. 基本类型 int a 100; int b 100; System.out.println(int (a b)); // 2. String String s1 hello; String s2 hello; String s3 new String(hello); System.out.println(字面量 (s1 s2)); System.out.println(new 对象 (s1 s3)); System.out.println(equals s1.equals(s3)); // 3. Integer缓存 Integer i1 127; Integer i2 127; Integer i3 128; Integer i4 128; System.out.println(127 (i1 i2)); System.out.println(128 (i3 i4)); System.out.println(128 equals i3.equals(i4)); // 4. 自定义类不重写equals User u1 new User(张三, 18); User u2 new User(张三, 18); System.out.println(自定义对象 (u1 u2)); System.out.println(自定义对象 equals u1.equals(u2)); } }输出结果整理成表格方便你对照比较场景结果原因int a int btrue基本类型直接比较数值字面量 s1 s2true字符串常量池中同一个对象字面量 s1 new String s3falsenew在堆中创建了新对象s1.equals(s3)trueString重写equals按字符内容比较Integer 127 127trueInteger缓存范围包含127Integer 128 128false超出缓存范围两个不同对象Integer 128.equals(128)trueInteger重写equals按int值比较User 对象 false两个new出来的不同对象User 对象 equalsfalse没重写equals默认退化为这里最容易被面试官点名的就是Integer的128问题。原因在Integer.valueOf的源码里public static Integer valueOf(int i) { if (i IntegerCache.low i IntegerCache.high) return IntegerCache.cache[i (-IntegerCache.low)]; return new Integer(i); }默认缓存范围是-128到127在这个范围内的Integer对象是复用的比较得到true超出范围后每次都会new一个新对象就变成false。要特别留意就算数值在缓存范围内只要你是手动new Integer(127)结果也是false因为new不走valueOf的缓存逻辑。3.3 重写equals和hashCode的标准模板自定义类想按业务逻辑比较就必须重写equals。但光重写equals不重写hashCode会埋下更大的坑。先看标准写法以User为例Override public boolean equals(Object o) { if (this o) return true; if (o null || getClass() ! o.getClass()) return false; User user (User) o; return age user.age Objects.equals(name, user.name); } Override public int hashCode() { return Objects.hash(name, age); }这段代码每一行都有讲究第一行this o是性能优化同一个对象直接返回true省去后续字段比较。第二行判断null和类型。用getClass() ! o.getClass()是一种严格的类型判断能避免子类对象和父类对象之间equals不对称。字段比较时基本类型age直接用对象类型name用Objects.equals这样即使name为null也不会抛空指针。hashCode用Objects.hash(name, age)基于参与equals判断的字段生成。这是Java的一条硬性约定两个对象equals相等hashCode必须相等。为什么必须有这个约定因为HashMap、HashSet这类散列集合先通过hashCode定位桶再用equals判断桶内元素。如果两个对象equals相等但hashCode不同它们会被放到不同桶里set去重和map查找都会失效。另外equals还要求满足五条规范自反性比如x.equals(x)必须true对称性比如x.equals(y)和y.equals(x)结果一致传递性一致性即对象不修改时多次调用结果稳定以及x.equals(null)必须是false。这些不需要背得一字不差但面试时能说出两三条会显得你是真懂。3.4 HashMap中equals与hashCode的协同工作把User对象当作Map的Key是理解这个问题的最佳场景。请看下面这段代码MapUser, String map new HashMap(); map.put(new User(张三, 18), 北京); System.out.println(map.get(new User(张三, 18))); // 没有重写的话输出null如果在User里不重写equals和hashCode这段代码大概率取不到北京。原因是两次new User(张三, 18)创建了两个不同的对象默认的hashCode来自Object两个对象的hashCode很可能不同HashMap在put和get时通过hashCode计算出不同的桶下标压根不会去调用equals。如果只重写equals而不重写hashCode情况会变成两个对象hashCode不同依然可能被分到不同桶get还是失败。只有在equals和hashCode都正确重写的情况下两个逻辑相等的对象hashCode相同才能进入同一个桶再由equals判断出这就是同一个Key。可以用一个图书馆找书的类比来理解hashCode决定你去几号书架找equals决定书架上哪本书才是你要找的那本。书架号错了连书都摸不到书架号对了但书名对不上同样借不到。这也是为什么我在面试时特别看重候选人能不能把这条链讲清楚。4. 实际开发中踩过的坑与排查实录4.1 陷阱一字符串比较导致的业务判断错误有段时间我接手维护一个老系统线上有个订单状态判断逻辑一直时灵时不灵。排查到最后发现代码里写的是if (order.getStatus() SUCCESS) { // 执行后续流程 }order.getStatus()来自数据库读取或者接口反序列化它和常量池里的字面量SUCCESS根本不是同一个对象。用比较结果是false但代码在本地测试时又经常能通过因为测试数据可能是直接编码在代码里的常量恰好指向常量池同一个对象。这种问题非常隐蔽因为它不是必现的而是跟字符串来源强相关。正确的写法是if (SUCCESS.equals(order.getStatus())) { // 执行后续流程 }或者用Objects.equals。原则就一条业务代码里比较字符串永远不要用。哪怕你确定字符串来自常量也不要赌因为你控制不了它后续会被谁new出来。4.2 陷阱二Map/Set容器中放了相等的对象却取不到另一个高频坑出现在实体类作为集合元素或Map Key的时候。我见过一个同事写了个去重逻辑用HashSet保存了一批用户对象业务上认为只要id相同就是同一个人结果集合里出现了两条id一样的记录。他当时很困惑Set不是天然去重吗问题出在User类只重写了equals没重写hashCode。两个id相同的User对象equals返回true但hashCode还是Object的默认实现两个对象hashCode不同于是HashSet把它们放进了不同桶根本感知不到重复。这个问题的解法很直接重写equals时一定同步重写hashCode且参与hashCode计算的字段要和equals保持一致。如果你用Lombok直接在类上标注Data或者EqualsAndHashCode它会自动生成这两个方法但记住下面要说到的继承场景要注意。4.3 陷阱三Lombok生成的equals在继承场景下的特殊行为Lombok很方便但用多了容易让人忽略细节。比如有个父类public class BaseEntity { private Long id; }子类Data public class User extends BaseEntity { private String name; }这时Lombok生成的equals和hashCode默认只考虑User类自己的字段也就是name不会包含父类的id。也就是说两个id完全不同的User对象只要name相同equals就会返回true。这在很多业务场景下是灾难。解决办法是在子类上显式写上EqualsAndHashCode(callSuper true)让它把父类字段也纳入比较。如果你的实体类有继承层级一定要检查这个点而不是无脑加Data。4.4 排查与预防从debug到工具方法根据我的经验碰到明明内容一样比较结果却不对的问题按照下面的顺序排查会很快先看代码里用的是什么还是equals。如果是确认比较双方在内存里是不是同一个对象。可以在IDEA断点处查看两个变量的id。如果id不同为false是预期行为。如果是equals检查当前类有没有重写equals和hashCode。在IDEA里左侧边栏能看到类的继承结构和重写标记。试试改用Objects.equals(a, b)看是不是有一方为null导致空指针或false。排查完之后回头检查这类比较是否应该封装成方法避免再次被误用。日常开发里我还会在代码审查时专门看两个地方字符串用没用到以及自定义类型有没有同时重写equals和hashCode。这两条检查习惯能挡住绝大多数比较类的隐性bug。5. 面试应答套路与高频追问5.1 一套可以直接用的应答框架如果你正在准备Java面试建议不要只背结论而是按照下面这个结构组织回答大概1分钟信息密度很高第一步先分类。基本类型用比较的是数值本身引用类型用比较的是引用地址。equals默认是Object里的方法源码就是return this obj所以不重写的情况下等于没区别。第二步举例子。String和Integer都重写了equals所以它们能按内容或数值比较。但使用比较String时要注意常量池和new的区别使用比较Integer时要注意-128到127的缓存范围。第三步主动引出hashCode。如果自定义类要重写equals必须同时重写hashCode否则在HashMap、HashSet里会出现查不到或去重失败的问题。两个相等对象的hashCode一定相等但hashCode相等不代表对象相等。这样回答下来你已经不是简单背八股而是展示了一条完整的推理链。面试官接下去问什么你都不会慌。5.2 高频追问与参考答案下面列几个我在面试中经常听到的追问以及参考思路追问参考回答思路两个new String(abc)用比较结果是什么false因为new在堆里创建了两个不同对象Integer i128; Integer j128; ij?false超出默认缓存范围equals才是true重写equals不重写hashCode会怎样散列集合下会出现重复元素或get失效违反对象相等约定String s new String(abc)创建了几个对象如果常量池已有abc创建1个堆对象如果没有创建常量池和堆2个对象为什么equals要求自反性、对称性为了保证集合和算法的行为可预期否则contain、remove等操作会乱套这里要特别提醒一下这些追问的目的不是让你背标准答案而是观察你能否从内存模型和源码角度去推理。哪怕你记不住全部细节能说出new会创建新对象和hashCode影响散列集合的桶定位这两个核心点就已经超过了大多数人。5.3 别把八股背死面试官真正想听什么我在面试现场最常听到的糟糕回答是把结论说得很顺但经不起任何一句为什么。这种候选人大概率是看了网上的Java面试大全临时背出来的。一问到为什么String两个字面量用是true就只会重复结论讲不出常量池的复用机制。真正能拿高分的人会用最朴素的语言把原理讲清楚。比如有一个候选人跟我说两个变量的就像比门牌号equals就像比屋里住的人String和Integer对屋里住的人做了自己的定义所以它们能比人是不是同一个但如果你不重写equals默认还是比门牌号。 这句话没有什么高大上的术语但说明他真的理解了。面试官要的就是这种把复杂东西讲明白的能力。6. 写在最后我的实战经验与建议6.1 日常开发中如何规避与equals的坑根据我这些年写代码和带团队的经验有几个习惯如果你能坚持基本可以避开90%的比较类问题字符串和包装类型的比较统一用Objects.equals不要用只有枚举类型可以放心用因为每个枚举常量在JVM里只有一个实例。自定义类作为集合元素或Map Key时必须同时关注equals和hashCode。用IDE自动生成或者Lombok都行但生成之后要检查参与比较的字段是否完整。如果你的实体类有继承关系Lombok生成equals时记得加callSuper true否则父类字段不参与比较很容易出线上事故。代码审查时把字符串是否用了作为一项固定检查简单又有效。6.2 再分享一个面试现场的加分技巧最后分享一个我亲测有效的表达技巧。回答这道题时不要一上来就放结论而是先说我分两种情况来看让面试官感觉到你思路清晰。然后按基本类型、引用类型、equals默认实现、重写规则、散列集合影响这个顺序讲。讲完习惯性补一句所以我在写自定义类的时候每次重写equals都会顺手看一眼hashCode确保两个方法保持同步。 这句话看起来简单却是面试官判断你是否有真实项目经验的重要信号。我在实际面试里只要候选人能主动提到hashCode契约和HashMap的关系就会在基础题这一项打上不错的分数。因为这说明他不只是在准备八股文而是在用工程思维理解Java。这道题背后真正的价值也从来不是让你记住一个比较结果而是让你养成透过现象看源码的习惯。希望这篇内容能帮你把这节课补扎实。
返回列表