一句话总结
== 比较栈中的值(基本类型比数值,引用类型比地址)→ equals() 默认比地址(Object 实现),但 String/Integer 等重写后比内容。重写 equals 必须同时重写 hashCode(HashMap 先用 hashCode 定位桶,再用 equals 判断相等)。Integer 有 -128~127 缓存,范围内 == 为 true。
初级理解
== 是关系运算符,比较的是两个变量的值是否相等。对于基本数据类型(int、long、boolean等),比较的就是数值;对于引用类型,比较的是内存地址(即两个引用是否指向同一个对象)。
equals() 是 Object 类的方法,默认实现也是比较内存地址(和 == 一样),但大多数类(如 String、Integer)都重写了 equals() 方法,改为比较对象的内容是否相等。
// == 比较基本类型:比较数值
int a = 10;
int b = 10;
System.out.println(a == b); // true
// == 比较引用类型:比较地址
String s1 = new String("hello");
String s2 = new String("hello");
System.out.println(s1 == s2); // false,两个不同对象
System.out.println(s1.equals(s2)); // true,内容相同
// 字符串常量池的影响
String s3 = "hello";
String s4 = "hello";
System.out.println(s3 == s4); // true,指向常量池同一个对象
一句话总结:== 比较的是栈中的值(基本类型比数值,引用类型比地址);equals() 默认比地址,但大多数类重写后比内容。
中级深入
重写 equals() 的规范:根据 Object.equals() 的 JavaDoc,重写 equals() 必须满足以下五个条件:
1. 自反性:x.equals(x) 必须返回 true
2. 对称性:x.equals(y) 和 y.equals(x) 结果必须一致
3. 传递性:如果 x.equals(y) 且 y.equals(z),则 x.equals(z)
4. 一致性:多次调用 equals() 结果不变(前提是对象信息没被修改)
5. 非空性:x.equals(null) 必须返回 false
重写 equals() 必须同时重写 hashCode():这是 Java 规范强制要求的。如果两个对象 equals() 返回 true,它们的 hashCode() 必须相等。反之不一定成立(哈希冲突)。
public class Person {
private String name;
private int age;
@Override
public boolean equals(Object o) {
if (this == o) return true; // 自反性优化
if (o == null || getClass() != o.getClass()) return false;
Person person = (Person) o;
return age == person.age && Objects.equals(name, person.name);
}
@Override
public int hashCode() {
return Objects.hash(name, age); // 必须同时重写
}
}
注意:如果只重写 equals() 不重写 hashCode(),将导致对象在 HashSet、HashMap 等哈希容器中出现"找不到"的诡异问题——两个 equals 为 true 的对象,因为 hashCode 不同被放到了不同的桶里。
高级拓展
HashMap 中 equals() 和 hashCode() 的联动机制:
HashMap 的 put 流程中,先通过 hashCode() 定位桶的位置,再通过 equals() 判断桶内链表/红黑树中是否存在相同的 key:
// HashMap.putVal() 简化逻辑
Node<K,V>[] tab = table;
int index = (n - 1) & hash; // 通过 hashCode 定位桶
// 在桶内遍历,通过 equals 判断 key 是否相同
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
// 找到了,替换 value
为什么重写 equals 必须重写 hashCode?因为 HashMap 先用 hashCode 定位桶。如果两个对象 equals 为 true 但 hashCode 不同,它们会被放到不同的桶里,HashMap 就无法正确识别它们是"同一个 key",导致重复存储。
Integer 的缓存陷阱:Integer 默认缓存了 -128 到 127 的对象(可通过 JVM 参数调整上限)。在这个范围内,== 比较会返回 true(因为指向缓存池中的同一个对象),超出范围则返回 false。
Integer a = 100;
Integer b = 100;
System.out.println(a == b); // true(在缓存范围内)
Integer c = 200;
Integer d = 200;
System.out.println(c == d); // false(超出缓存范围)
System.out.println(c.equals(d)); // true(始终比内容)
Lombok @Data 的坑:@Data 注解会自动生成 equals() 和 hashCode(),默认使用所有非静态字段。如果实体类中有懒加载的关联对象(如 JPA 的 @ManyToOne),可能导致意外的数据库查询或循环调用。建议使用 @EqualsAndHashCode(exclude = {"关联字段"}) 排除不需要的字段。
面试加分项:能说出 HashMap 中 hashCode 定位桶 + equals 判断相等的联动机制,说明你看过 JDK 源码,面试官会对你印象深刻。
实战场景
场景:Lombok @Data 的 equals/hashCode 坑
// 反例:@Data 默认使用所有字段,JPA 懒加载会触发额外查询
@Entity
@Data
public class Order {
@Id private Long id;
@ManyToOne(fetch = FetchType.LAZY)
private User user; // equals/hashCode 会触发 user 的查询!
}
// 正例:排除懒加载字段
@Entity
@Data
@EqualsAndHashCode(exclude = "user")
public class Order {
@Id private Long id;
@ManyToOne(fetch = FetchType.LAZY)
private User user;
}
场景:集合操作中的 equals 陷阱
// 问题:只重写 equals 不重写 hashCode
Set<Person> set = new HashSet<>();
Person p1 = new Person("张三", 20);
Person p2 = new Person("张三", 20);
set.add(p1);
System.out.println(set.contains(p2)); // false!因为 hashCode 不同
// 解决:同时重写 equals 和 hashCode
// 或使用 IDE 生成 / Lombok @EqualsAndHashCode
面试模拟
Q:为什么重写 equals 必须重写 hashCode?
A:因为 HashMap/HashSet 等哈希容器先用 hashCode 定位桶,再用 equals 判断相等。如果两个对象 equals 为 true 但 hashCode 不同,它们会被放到不同桶中,导致 contains() 返回 false、重复存储等问题。这是 Java 规范强制要求的。
Q:Integer 用 == 比较有什么坑?
A:Integer 默认缓存 -128~127 的对象,范围内 == 比较返回 true(指向缓存池同一对象),超出范围返回 false(不同对象)。比较 Integer 值始终用 equals(),不要依赖 ==。