☰
Java 的 == 和 equals 到底有什么区别?用 String、Integer 和自定义对象一次讲清
2026/10/5 11:04:59 网站建设 项目流程

快速认识

==和equals不是“一个比较地址,一个比较内容”这么简单。

更准确的规则是:

  • 两边是基本类型时,==比较值。
  • 两边是对象引用时,==判断它们是否指向同一个对象。
  • equals是一个方法。Object里的默认实现就是引用比较,但String、包装类等会重写它。
  • 自定义类如果只重写equals、不重写hashCode,放进HashSet或作为HashMap的 Key 时仍然可能出错。

可以先记住一张判断表:

场景推荐写法原因
比较两个int、long、doublea == b基本类型没有equals
比较两个字符串的内容a.equals(b)或Objects.equals(a, b)字符串的equals已按内容实现
判断两个引用是否为同一个对象a == b这正是引用比较的语义
枚举值比较enumValue == OtherEnum.VALUE枚举常量是单例,==更安全也更直观
可能为null的对象值比较Objects.equals(a, b)避免空指针
自定义实体按业务字段判等重写equals和hashCode保证集合和 Map 行为一致

先用一个最小例子看结果:

Stringa="Java";Stringb="Java";Stringc=newString("Java");System.out.println(a==b);// trueSystem.out.println(a==c);// falseSystem.out.println(a.equals(c));// true

a == b为true,是因为两个字面量可以复用同一个字符串对象。c是新创建的对象,所以a == c为false。它们的内容都相同,所以a.equals(c)为true。

拓展

一、先区分基本类型和引用类型

基本类型保存的是具体数值,引用类型保存的是指向对象的引用。

intleft=10;intright=10;System.out.println(left==right);// true

这里比较的是两个int的值,所以结果是true。

对于引用:

Stringleft=newString("Java");Stringright=newString("Java");System.out.println(left==right);// falseSystem.out.println(left.equals(right));// true

new String创建了两个不同的对象。==判断它们是否为同一个对象,结果是false;equals比较字符串内容,结果是true。

所以,不要把“对象用equals”背成无条件规则。真正要问的是:我现在比较的是对象身份,还是对象的业务值?

二、为什么字符串字面量的 == 有时是 true

直接写出的字符串字面量会进入字符串常量池。同一组在编译期确定的字符串内容,可以被两个变量引用到同一个对象。

Stringa="Java";Stringb="Java";Stringc="Ja"+"va";System.out.println(a==b);// trueSystem.out.println(a==c);// true

"Ja" + "va"的两个操作数都是编译期常量,编译器会在编译阶段完成拼接,所以它仍然可以复用"Java"的常量对象。

但下面两种情况不一样:

Stringprefix="Ja";Stringd=prefix+"va";Stringe=newString("Java");System.out.println(a==d);// falseSystem.out.println(a==e);// falseSystem.out.println(a.equals(d));// trueSystem.out.println(a.equals(e));// true

prefix是变量,它的值要到运行时才能确定。这里是否创建新对象、是否进入常量池,不能只凭肉眼猜。实际写业务代码时,字符串内容比较就统一使用equals或Objects.equals,不要依赖常量池带来的==结果。

三、Integer 的 127 和 128

下面这段代码很容易让初学者疑惑:

Integera=127;Integerb=127;Integerc=128;Integerd=128;System.out.println(a==b);// trueSystem.out.println(c==d);// falseSystem.out.println(c.equals(d));// true

自动装箱会调用类似Integer.valueOf(127)的方法。HotSpot 常见实现会缓存-128到127之间的Integer对象,所以a和b可能指向缓存中的同一个对象;128超出这个常见范围,c和d可能是两个不同对象。

这里要特别注意两点:

  1. 包装类比较的是对象,不能把==当成数值比较。
  2. Integer和Long即使数值相同,equals也会因为类型不同而返回false。
Integercount=1000;LongsameValue=1000L;System.out.println(count.equals(sameValue));// false

业务代码中比较包装类数值,优先使用equals,并且确认两边类型一致。需要做数值计算时,先明确是否可能为null,再决定如何拆箱或转换。

四、自定义对象必须自己说明“什么叫相等”

如果类没有重写equals,它继承的是Object.equals,效果等同于引用比较。

classPlainUser{privatefinalStringname;PlainUser(Stringname){this.name=name;}}PlainUserleft=newPlainUser("Tom");PlainUserright=newPlainUser("Tom");System.out.println(left.equals(right));// false

两个用户的name相同,但这是两个不同对象,所以默认比较结果仍然是false。如果业务规定“用户名相同就是同一个用户”,就需要自己重写。

importjava.util.Objects;classNamedUser{privatefinalStringname;NamedUser(Stringname){this.name=name;}@Overridepublicbooleanequals(Objectother){if(this==other){returntrue;}if(!(otherinstanceofNamedUser)){returnfalse;}NamedUserthat=(NamedUser)other;returnObjects.equals(name,that.name);}@OverridepublicinthashCode(){returnObjects.hash(name);}}

这段代码做了三件事:

  1. 先判断是否为同一个对象,减少后续比较。
  2. 判断类型是否匹配。
  3. 用Objects.equals比较参与判等的字段,顺便处理字段为null的情况。

为什么还要重写hashCode?因为HashSet、HashMap会先根据哈希值寻找桶,再用equals确认是否相等。如果两个对象被判定为equals,但哈希值不同,集合就可能把它们放进不同位置。

Set<NamedUser>users=newHashSet<>();users.add(newNamedUser("Tom"));System.out.println(users.contains(newNamedUser("Tom")));// true

这里的contains为true,不是因为两个对象是同一个对象,而是因为类已经定义了“姓名相同就算相同用户”,并且equals与hashCode使用了同一组字段。

五、判断路径可以简化成这样

写代码时不需要每次都想一遍底层实现,可以按下面的顺序判断:

两边是基本类型 -> 用 == 两边是对象 -> 需要判断是不是同一个对象?用 == -> 需要按内容或业务字段比较?用 equals / Objects.equals -> 比较的是枚举常量?用 == -> 自己定义的实体需要进入 Set / Map?重写 equals + hashCode

六、几个容易踩的坑

第一个坑:对可能为 null 的变量直接调用 equals。

Stringleft=null;Stringright="Java";// System.out.println(left.equals(right)); // NullPointerExceptionSystem.out.println(Objects.equals(left,right));// false

如果你能确定常量非空,也可以写成"Java".equals(variable)。如果两边都可能为空,Objects.equals更清楚。

第二个坑:只重写 equals,不重写 hashCode。

这在普通if判断里可能看不出来,但加入HashSet或HashMap后就会暴露。只要重写其中一个,就应该同时检查另一个。

第三个坑:BigDecimal 不能用 equals 判断数值相等。

BigDecimala=newBigDecimal("1.0");BigDecimalb=newBigDecimal("1.00");System.out.println(a.equals(b));// falseSystem.out.println(a.compareTo(b));// 0

equals会同时考虑数值和精度,compareTo比较数值大小。金额比较采用哪个方法,取决于业务是否关心精度表示。不要看到false就立刻认为compareTo永远更合理,先把业务口径写清楚。

第四个坑:数组用 equals 比较内容。

数组没有按元素重写equals。要比较两个数组的内容,应使用Arrays.equals;二维数组使用Arrays.deepEquals。

七、练习题

先不要运行代码,自己写下结果,再对照输出。

练习一:字符串

Stringa="java";Stringb=newString("java");Stringc="ja"+"va";System.out.println(a==b);System.out.println(a==c);System.out.println(a.equals(b));

重点不是背答案,而是解释每一行为什么是true或false。

练习二:Integer

Integera=100;Integerb=100;Integerc=200;Integerd=200;System.out.println(a==b);System.out.println(c==d);System.out.println(c.equals(d));

再把这四个变量改成new Integer(...)的写法,比较结果会如何变化?注意new Integer已经不再推荐使用,这里只用来观察对象身份。

练习三:自定义对象

创建一个Student类,包含id和name。规定id相同就是同一学生,完成:

  1. 重写equals和hashCode。
  2. 创建两个id相同、name不同的对象。
  3. 放入HashSet,观察集合大小。
  4. 调用contains,解释结果。
  5. 删掉hashCode,重新运行,观察变化。

练习四:找错误

下面这段代码可能在什么时候出错?

Map<String,Integer>scores=newHashMap<>();Stringkey=null;scores.put(key,90);System.out.println(scores.get(null));

先查HashMap对nullKey 的说明,再结合自己的 JDK 版本运行验证。不要把“HashMap 一定不能存 null”当成未经核对的结论。

八、B站和练习资源怎么用

B站适合用它看老师怎么把对象、引用和集合串起来,但不要只开着视频“听懂”。每看完一段,就把示例里的一个变量改掉,重新运行,看看结果是否和预测一致。

可以按这个顺序检索:

  1. B站搜索“Java equals 和 == 的区别”,先看一个 10 到 20 分钟的入门讲解。
  2. 搜索“Java hashCode equals”,确认集合中为什么需要成对重写。
  3. 搜索“Java String 常量池”,理解字面量和new String的差异。
  4. 搜索“Java Integer 缓存”,观察自动装箱的执行路径。
  5. 回到代码,把文章中的四个练习全部跑一遍。

可直接打开这些入口:

  • B站:Java equals 和 == 的区别
  • B站:Java hashCode equals
  • Oracle:Object.equals
  • Oracle:Object.hashCode
  • HackerRank Java 练习
  • CodingBat Java 练习

最后给一个实用规则:如果比较的是对象身份,用==;如果比较的是值或业务字段,用equals或Objects.equals;如果自定义对象要进入HashSet、HashMap,把equals和hashCode一起检查。先把这三个判断用到自己的代码里,比一次背完所有细节更有用。

标签:Java、equals、Java基础、字符串、HashMap

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询