- 0
- 349 words
Java 里有一个类,是所有类的最终祖先——上到 String、下到你自定义的 Dog 类,都是它的子孙。它就是 java.lang.Object。
Object 在你刚学 Java 的时候可能没怎么在意它——因为你定义类从来不写 extends Object,但编译器默默地帮你补上了。Java 里写 class A {},编译后等价于 class A extends Object {}。
Object 类里面一共就 11 个方法(除了过时的 finalize() 也就 10 个)。这篇我们先聊两个面试问得最多的:equals() 和 hashCode()。
一、equals() VS ==
这问题但凡面过 Java 的都避不开:== 和 equals() 有什么区别?
1.1 原理
==:比较基本类型的值,或者引用类型的内存地址(即是不是同一个对象)equals():默认也是比较地址(Object 的实现就是==),但很多类重写过它,改为比较内容
String s1 = new String("hello");
String s2 = new String("hello");
System.out.println(s1 == s2); // false —— 两个不同的对象
System.out.println(s1.equals(s2)); // true —— String 重写了 equals,比较内容
这个坑我有一次在写登录校验的时候踩过:从数据库查出来的密码和用户输入的密码用 == 比较,结果怎么也登不上去……排查了半个小时才发现是 == 的问题。String 是引用类型,不能用 == 比较内容。
1.2 自己重写 equals()
默认的 equals() 只比较地址,所以自定义的类需要自己重写:
public class Student {
private String id; // 学号
private String name;
private int age;
public Student(String id, String name, int age) {
this.id = id;
this.name = name;
this.age = age;
}
@Override
public boolean equals(Object obj) {
// 1. 比较地址:如果就是同一个对象,直接返回 true
if (this == obj) return true;
// 2. 判空和类型检查
if (obj == null || getClass() != obj.getClass()) return false;
// 3. 转型后按字段比较
Student other = (Student) obj;
return age == other.age
&& Objects.equals(id, other.id)
&& Objects.equals(name, other.name);
}
}
重写 equals() 时要遵守几条约定(Java 语言规范写的):
- 自反性:
x.equals(x)必须返回 true - 对称性:
x.equals(y)等于y.equals(x) - 传递性:如果
x.equals(y)且y.equals(z),那x.equals(z)也必须 true - 一致性:只要对象没变,多次调用
equals()结果应该一致 - 非空性:
x.equals(null)必须返回 false
其实真要手写的话,推荐用 Objects.equals() 工具方法,它能自动处理 null:
// Objects.equals(a, b) 等价于 (a == b) || (a != null && a.equals(b))
// 不用手写空指针判断了
二、hashCode()——哈希表的基础
2.1 什么是哈希值?
hashCode() 返回一个 int 整数,可以理解为对象的一个数字摘要。默认实现(Object 类)是根据内存地址算出来的一个值。
但它真正的威力在哈希表里——HashMap、HashSet、HashTable 这些集合用 hashCode 来决定对象存到哪个桶(bucket)里。
2.2 equals 和 hashCode 的约定
这是 Java 里最最最重要的约定之一:
如果两个对象
equals()返回 true,那么它们的hashCode()必须 相等。
如果两个对象hashCode()相等,equals()不一定 true(哈希冲突)。
简单理解:hashCode 决定你住哪个小区,equals 决定你是不是同一个人。两个人住同一个小区不代表是同一个人,但同一个人一定住在同一个小区。
所以重写 equals() 的时候必须同时重写 hashCode()——否则你的对象放进 HashSet 或 HashMap 会出大问题。
来看反面教材:
public class Student {
String id;
String name;
// 只重写了 equals,没重写 hashCode ❌
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Student s = (Student) o;
return Objects.equals(id, s.id);
}
}
// 使用 HashSet
Set<Student> set = new HashSet<>();
set.add(new Student("001", "张三"));
System.out.println(set.contains(new Student("001", "张三")));
// 输出:false ❌❌❌
// 明明两个 "001" 学号的学生 equals 相等,但因为 hashCode 不同,
// 存入和查找时去了不同的桶,HashSet 根本找不到那个对象!
这个 Bug 我在实习的时候亲自踩过——自定义对象放了 HashSet 里,怎么也查不到,翻了半天文档才意识到 hashCode 没重写。后来我给自己定了个铁律:记性再差,也要记住重写 equals 就必须重写 hashCode。
2.3 正确重写 hashCode
标准写法(Java 7+ 推荐 Objects.hash()):
public class Student {
String id;
String name;
int age;
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Student s = (Student) o;
return age == s.age
&& Objects.equals(id, s.id)
&& Objects.equals(name, s.name);
}
@Override
public int hashCode() {
// 用 equals 用到的所有字段来计算 hashCode
return Objects.hash(id, name, age);
}
}
Objects.hash() 内部会把这几个字段组合成一个 hash 值。注意:hashCode 计算必须用 equals 中比较的那些字段,多一个少一个都不对。
三、IDE 一键生成:别手写
现在正经 Java 开发谁还手写 equals() 和 hashCode()?
IntelliJ IDEA / Eclipse / VS Code 都支持一键生成:右键 → Generate → equals() and hashCode() → 选好字段 → 搞定。
但理解原理很重要——你总得知道 IDE 给你生成了什么,以及为什么要这么写,不然面试一问就露馅。
四、小结
==比较基本类型的值/引用类型的地址;equals()默认比较地址,但可重写为比较内容- 重写
equals()要满足自反性、对称性、传递性、一致性、非空性 - 重写
equals()必须重写hashCode(),否则HashMap/HashSet行为异常 - 优先用
Objects.equals()和Objects.hash()工具方法,避免自己造轮子 - IDE 可以生成,但面试要手写——别光会用,原理得懂
下一篇我们把 Object 类剩下的方法讲完——toString()、clone()、getClass()、finalize(),还有一个跟线程相关的 notify()/wait() 系列放多线程再说。