• == 与equals 的主要区别是:== 常用于比较原生类型,而 equals() 方法用于检查对象的相等性。

  • 另一个不同的点是:如果 == 和 equals() 用于比较对象,当两个引用地址相同,== 返回 true。而 equals() 可以返回 true 或者 false 主要取决于重写实现。最常见的一个例子,字符串的比较,不同情况 == 和 equals() 返回不同的结果。

两点结论正确但偏薄。要答出深度,需要补三块:字符串常量池这个经典案例的完整推演、equals 重写必须连带重写 hashCode 的契约、以及 Integer 缓存这类包装类的陷阱。

语义层:值比较 vs 引用比较

  • == 是运算符:基本类型比较值;引用类型比较引用(地址)——即”是不是同一个对象”
  • equals 是 Object 的方法:默认实现就是 this == obj(比引用);语义由重写者定义,String、Integer、LocalDate 等都重写为”内容相等”

所以”equals 比内容”不是语言保证,而是类的契约承诺;自己写的类不重写 equals,equals 就退化为 ==。

字符串案例完整推演

String a = "hello";
String b = "hello";
String c = new String("hello");
String d = c.intern();

a == b // true:字面量进常量池,a/b 指向池中同一对象
a == c // false:new 在堆上建新对象
a.equals(c) // true:String 重写 equals 比字符序列
a == d // true:intern 返回常量池引用

JDK7 后常量池在堆中,intern 语义为”池中有则返回池引用,无则入池并返回”。理解常量池,才能解释为什么字面量 == 为 true 而 new 出来为 false。

重写 equals 的契约与 hashCode 连带

equals 重写必须满足:自反、对称、传递、一致、对 null 返回 false。而重写 equals 必须同时重写 hashCode,契约是:equals 相等的对象 hashCode 必须相等。

违反的后果直接体现在哈希容器:

class Point { int x, y; /* 只重写 equals,未重写 hashCode */ }
Map<Point, String> m = new HashMap<>();
m.put(new Point(1, 2), "A");
m.get(new Point(1, 2)); // null!hashCode 不同 → 落到不同桶 → 找不到

HashMap 先比 hashCode 定位桶、再比 equals 定位节点;hashCode 不一致时 equals 根本没机会执行。这就是”equals 与 hashCode 是同一个契约的两面”。

包装类与缓存陷阱

Integer i1 = 127, i2 = 127;
Integer i3 = 128, i4 = 128;
i1 == i2 // true:IntegerCache 缓存 -128~127
i3 == i4 // false:超出缓存,两个对象
i3.equals(i4) // true

同理 Long/Short/Byte/Character 也有缓存区间。结论:对象比较永远用 equals(或 Objects.equals 防 NPE),== 只用于基本类型与判 null/单例身份。

实践清单

  • 比较用 Objects.equals(a, b):null 安全,避免 NPE
  • IDE 生成 equals/hashCode 成对重写,字段选取与业务一致性定义一致
  • 作为 Map key / Set 元素的类必须不可变或保证 hashCode 稳定(可变对象改字段后”从集合中消失”是经典 bug)
  • 枚举比较可用 ==(单例语义),但统一用 equals 也无害且更安全

== 与 equals 的区别,表层是运算符与方法,里层是引用语义与内容语义的分工;而 hashCode 契约、常量池、包装类缓存这三个细节,决定了这个知识点在真实代码里会不会咬人。