==和equals有什么区别?看不懂隔着网线过来打我

==equals 这个问题,简直是程序员面试和日常开发中的“老朋友”了,几乎每隔一段时间就会在代码里和它不期而遇地打个招呼。别看它基础,但真要说清楚,特别是后面那个 hashCode,刚入门的时候我确实也踩了不少坑。

今天,我就以一个过来人的身份,跟大家聊聊我当初是怎么一步步搞懂这哥仨的。

面试官最爱问的==equals,我到底踩了多少坑?

缘起:一个让我百思不得其解的 Bug

还记得那是刚工作不久,我在做一个用户管理的功能。有个需求是,从数据库里查出来一个用户对象,然后跟一个本地创建的、信息完全相同的用户对象做比较,判断它们是不是“同一个人”。

当时我的代码差不多是这个样子的:

public class User {
    private Long id;
    private String username;

    // 构造函数、getter/setter省略...
}

// ... 在我的业务代码里
User userFromDB = new User(1L, "xiaowu");
User userFromFrontend = new User(1L, "xiaowu");

if (userFromDB == userFromFrontend) {
    System.out.println("是同一个用户!");
} else {
    System.out.println("奇怪,明明信息都一样,怎么就不是同一个用户呢?");
    // 输出结果:奇怪,明明信息都一样,怎么就不是同一个用户呢?
}

看到这个输出,我当时就懵了。ID 和用户名明明都一样啊,这俩 User 对象怎么看都应该是“相等”的,为什么 == 会告诉我它们不相等?这就是我踩的第一个坑。

探索之旅:== 到底在比什么?

没办法,只能硬着头皮去查资料。折腾了一番后,我总算搞明白了。

== 这个操作符,它的行为非常“耿直”,得分情况看:

  1. 对于基本数据类型 (Primitive Types):比如 int, long, boolean 这些,== 比较的就是它们的值。int a = 10; int b = 10;,那 a == b 就是 true,这没啥好说的。

  2. 对于引用数据类型 (Reference Types):比如我们自己写的 User 类,还有 StringInteger 这些。== 比较的是 内存地址

啥是内存地址?你可以把它想象成每个对象在内存中的“门牌号”。

new User(...) 这个操作,就相当于在内存里盖了一栋新房子,并把门牌号交给你。我的代码里 new 了两次,就等于盖了两栋独立的房子。虽然这两栋房子里面的装修(id、username)一模一样,但它们的门牌号是绝对不一样的。

所以,userFromDB == userFromFrontend 其实是在问:“这两把钥匙能打开同一扇门吗?” 答案显然是不能。

这就是 == 的本质:检查两个引用是否指向内存中的同一个对象。

柳暗花明:equals 方法才是正解?

知道了 == 不行,我立马想到了 equals 方法。毕竟,写 String 的时候,我们都是用 equals 来比较字符串内容的。于是我赶紧把代码改了:

if (userFromDB.equals(userFromFrontend)) {
    System.out.println("这次总该是同一个用户了吧!");
} else {
    System.out.println("怎么还是不行?!");
    // 输出结果:怎么还是不行?!
}

结果,又打脸了。这 equals 怎么也“叛变”了?

万念俱灰之下,我只好按住 Ctrl 点击 equals 方法,想看看它底层到底是个啥。不看不知道,一看吓一跳,Object 类的 equals 方法源码长这样:

// 这是所有类的祖宗 Object 类里的 equals 方法
public boolean equals(Object obj) {
    return (this == obj);
}

破案了!如果我们自己写的类没有“重写” (Override) equals 方法,那它默认继承的就是 Object 类的这个版本,而这个版本骨子里还是在用 == 做比较!

所以,要想比较两个 User 对象的内容是否相等,我们必须亲自动手,告诉程序什么是“内容相等”。

// 在 User 类里重写 equals 方法
@Override
public boolean equals(Object o) {
    // 1. 先用 == 判断是不是同一个对象,如果是,那肯定相等
    if (this == o) return true;
    
    // 2. 判断 o 是不是 null,或者类型和我不一样,那肯定不等
    if (o == null || getClass() != o.getClass()) return false;
    
    // 3. 把 o 强转成 User 类型,然后比较我们关心的字段
    User user = (User) o;
    return java.util.Objects.equals(id, user.id) &&
           java.util.Objects.equals(username, user.username);
}

加上这段代码后,userFromDB.equals(userFromFrontend) 终于返回了 true。长舒一口气!

终极陷阱:hashCode 的致命一击

本以为故事到这里就结束了,结果没过多久,我在用 HashMap 或者 HashSet 时,又遇到了一个更诡异的问题。

我想用 HashSet 来存一堆用户,实现去重的效果。

Set<User> userSet = new HashSet<>();
User user1 = new User(1L, "xiaowu");
User user2 = new User(1L, "xiaowu");

System.out.println("user1.equals(user2): " + user1.equals(user2)); // 输出 true

userSet.add(user1);
userSet.add(user2);

System.out.println("Set size: " + userSet.size()); // 期望是1,结果是 2

我明明重写了 equals 方法,user1user2 已经是“相等”的了,为什么 HashSet 还是把两个都加进去了?它不是应该去重吗?

这就是因为我只重写了 equals,却忘了重写 hashCode

要理解这个问题,得先知道 HashSetHashMap 这些集合是怎么工作的。你可以把 HashMap 想象成一个大仓库,里面有很多货架(bucket)。

  • 当你存一个东西(比如 user1)进去时,HashMap 会先问这个东西一个数:user1.hashCode()。这个返回的数字(哈希码)就决定了 user1 该放到哪个货架上。
  • 当你再存 user2 时,HashMap 同样计算 user2.hashCode(),找到对应的货架。如果那个货架上已经有东西了,它才会用 equals() 方法去一个个比较,看看是不是已经存在一个一模一样的了。

关键点来了:如果你不重写 hashCode,它默认也是根据对象的内存地址来生成一个几乎唯一的数字。

这就导致 user1user2 虽然 equals 相等,但它们的 hashCode() 返回了两个完全不同的值。HashSet 就认为它们应该被放在两个不同的“货架”上,压根就没机会用 equals 去比较它们,理所当然地认为这是两个不同的对象。

黄金法则:equalshashCode 的约定

Java 中有一条神圣的、不可破坏的规定:

如果两个对象通过 equals() 方法比较是相等的,那么它们的 hashCode() 方法必须返回相同的值。

反过来不一定成立:hashCode 相同,equals 不一定为 true(这叫哈希冲突),但 equalstruehashCode 必须相同。

所以,最完美的解决方案是,在重写 equals 的同时,也根据相同的字段来重写 hashCode

// 在 User 类里,补上 hashCode 方法
@Override
public int hashCode() {
    // 使用 Objects.hash 工具类可以很方便地根据字段生成一个哈希码
    return java.util.Objects.hash(id, username);
}

加上这个 hashCode 方法后,上面那个 HashSet 的例子就能正常工作了,userSet.size() 会正确地输出 1

我的感悟

== 的地址比较,到 equals 的内容比较,再到 hashCode 为集合服务的本质,这一路踩坑下来,让我对 Java 的对象模型有了更深的理解。

  • ==:对比的是“身份”,是不是内存里的同一个东西。
  • equals():对比的是“内涵”,内容是不是我们认为的“相等”。(前提是你得重写它)
  • hashCode():是 equals() 的“开路先锋”,在海量数据里快速定位到“可能相等”的小团体,是保证 HashMapHashSet 这类集合能够高效运行的基石。

记住这个铁律:但凡你要重写 equals,请务必、一定、必须把 hashCode 也一起重写了! 否则,一旦你的对象被放进哈希集合里,各种诡异的 Bug 就会找上门来。

不知道大家在面试或者工作中是不是也经常被问到这个问题?你们有没有遇到过因为忘了重写 hashCode 导致的诡异 bug?欢迎在评论区留言讨论啊!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值