答案是:==比较值或内存地址,equals()比较逻辑内容,重写equals()需遵守五契约并同步重写hashCode()。
==在 Java 里主要比较的是值或内存地址。对于基本数据类型(如
int,
char,
boolean),它比较的是它们实际的数值。但对于对象类型,
==比较的是这两个引用是否指向了内存中的同一个对象实例。而
equals()方法,它的设计初衷就是为了比较两个对象的内容是否逻辑上相等。默认情况下,
Object类里的
equals()实现和
==行为一样,也是比较内存地址,但很多类,比如
String、
Integer等,都重写了这个方法,让它比较对象内部的数据。
说实话,每次讲到
==和
equals()的区别,我总觉得像是在重温 Java 基础的“九阳真经”第一页。但它确实太重要了,因为稍有不慎,就可能踩到坑里,导致程序行为不符合预期。
咱们先从最直观的看。如果你有两个
int变量
a = 5和
b = 5,那么
a == b肯定是
true。这里
==就是老老实实地比对那两个“5”这个数值。没毛病。
但如果换成对象呢?假设我们有
String s1 = new String("hello") 和 String s2 = new String("hello")。这时候 s1 == s2会是
false。为什么?因为
new String()每次都会在堆内存里创建一个全新的
String对象。所以
s1和
s2虽然内容都是 "hello",但它们是两个不同的对象实例,内存地址不一样。而
==关注的就是这个内存地址。
这时候
equals()就登场了。对于
String类,它早就被重写了,所以
s1.equals(s2)会返回
true。
String类的
equals()不看内存地址,它会逐个字符地比较两个字符串的内容。这才是我们大多数时候希望的“相等”概念,对吧?我们关心的是“你是不是叫张三”,而不是“你是不是坐在我旁边的张三”。
再来个例子,如果你自己写一个
Person类,里面有
name和
age字段。如果你不重写
equals()方法,那么
new Person("Alice", 30) == new Person("Alice", 30) 肯定是 false,甚至
new Person("Alice", 30).equals(new Person("Alice", 30)) 也会是 false。因为默认的
equals()行为就是
==的行为。要让这两个“Alice, 30”在逻辑上相等,你就得手动去重写
equals(),告诉 Java 虚拟机,当
name和
age都一样的时候,这两个
Person对象就算相等。这其实就是赋予了对象“内容相等”的语义。
有时候,我会看到一些新手,甚至是老手,在比较
Integer对象时犯错。比如
Integer i1 = 100; Integer i2 = 100;此时
i1 == i2可能是
true。但
Integer i3 = 200; Integer i4 = 200;此时
i3 == i4却可能是
false。这背后涉及到
Integer的缓存机制(-128到127之间的整数会被缓存)。这种“看脸”的相等性判断,真的让人头疼。所以,对于对象包装类,永远用
equals()才是最稳妥的。
总结一下,
==就像是身份证号比对,看是不是同一个人(内存地址)。
equals()则是内容比对,看是不是同一个人(逻辑内容)。理解这个核心差异,基本上就抓住了关键。
equals()方法时,有哪些“坑”是必须注意的?
重写
equals()绝对不是件小事,它有一套严格的契约,一旦违反,程序行为就可能变得诡异莫测。我见过太多因为
equals()没写对,导致
HashMap、
HashSet行为异常的案例。
最基础的,就是
equals()方法必须满足以下五个特性,这被称为“
equals()契约”:
null的对象
x,
x.equals(x)必须返回
true。这很直观,自己和自己肯定相等。
x.equals(y)返回
true,那么
y.equals(x)也必须返回
true。这个性质经常被忽略,尤其是在涉及继承的时候。比如,
ColorPoint和
Point,如果
ColorPoint的
equals()只比较
x, y坐标,那么
new Point(1,2).equals(new ColorPoint(1,2,Color.RED))可能会是
true,但反过来
new ColorPoint(1,2,Color.RED).equals(new Point(1,2))却可能因为
ColorPoint内部的
color字段而返回
false,这就违反了对称性。通常的建议是,如果想在子类中添加新的字段,就不要扩展
equals()的行为,而是使用组合(composition)而不是继承。
x.equals(y)返回
true,并且
y.equals(z)返回
true,那么
x.equals(z)也必须返回
true。这在多层继承或者复杂对象比较时尤其容易出错。
x.equals(y),结果都应该保持一致。这意味着
equals()的判断逻辑不应该依赖于随机数、当前时间或者网络状态这些不确定的因素。
null的对象
x,
x.equals(null)必须返回
false。这是为了避免
NullPointerException,也是一个基本的防御性编程习惯。
除了这五大契约,还有一个非常关键的点:重写 equals()
时,必须同时重写 hashCode()
方法。 这是因为
HashMap、
HashSet等基于散列(hash)的数据结构,都是先通过
ha来快速定位对象的存储位置,再通过shCode()
equals()来确认对象是否真的相等。如果两个逻辑上相等的对象有不同的
hashCode,那么它们在
HashMap中就可能被存储在不同的位置,导致
get()方法找不到本应存在的数据。反之,如果
hashCode()相同,
equals()不同,那性能会下降,但至少功能上不会出错。但如果
equals()相同,
hashCode()不同,那就彻底乱套了。
所以,在实现
equals()时,我通常会遵循一个模式:
@Override
public boolean equals(Object o) {
if (this == o) return true; // 自反性,性能优化
if (o == null || getClass() != o.getClass()) return false; // 非空性,类型检查
// 或者用 o instanceof MyClass,但这在继承场景下可能带来对称性问题,getClass() 更严格
MyClass myClass = (MyClass) o;
// 逐一比较关键字段
return field1 == myClass.field1 &&
Objects.equals(field2, myClass.field2) && // 使用 Objects.equals 处理 null 值
// ... 其他字段
;
}
@Override
public int hashCode() {
// 使用 Objects.hash() 方便地生成哈希码,它能处理 null 值
return Objects.hash(field1, field2 /*, ... 其他字段 */);
}这里
Objects.equals()和
Objects.hash()是 Java 7 引入的工具类,它们能很好地处理
null值,避免了手动写
if (field1 != null ? field1.equals(myClass.field1) : myClass.field1 == null)这样的繁琐代码,大大简化了重写过程,也减少了出错的可能。
String、
Integer等包装类要重写
equals(),它们对
HashMap和
HashSet有什么影响?
String和
Integer这些类重写
equals()方法,完全是为了符合我们人类对“相等”的直观理解。试想一下,如果两个字符串内容都是 "hello",但因为它们