equals、hashCode 与对象克隆
== 和 equals 的区别
Java 中有两种"相等"的判断方式:
==:比较的是内存地址(引用是否指向同一个对象)equals():比较的是逻辑内容(两个对象在业务上是否相等)
// 字面量写法:都指向常量池中的同一个对象
String s1 = "hello";
String s2 = "hello";
System.out.println(s1 == s2); // true ← 同一个常量池对象
System.out.println(s1.equals(s2)); // true ← 内容也相同
// new 写法:强制在堆上创建新对象,绕过常量池
String a = new String("hello");
String b = new String("hello");
System.out.println(a == b); // false ← 堆上两个不同的对象
System.out.println(a.equals(b)); // true ← 但内容相同
Object 类中 equals() 的默认实现就是 ==:
// java.lang.Object
public boolean equals(Object obj) {
return (this == obj);
}
如果不重写 equals(),那它和 == 的行为完全一样。String、Integer 等类之所以能比较内容,是因为它们重写了 equals()。
重写 equals 的五条约定
重写 equals() 必须遵守 Object 类文档中规定的合同(contract),否则在集合等依赖 equals() 的场景中会出现诡异的 bug:
| 约定 | 含义 | 反面后果 |
|---|---|---|
| 自反性 | x.equals(x) 必须返回 true |
对象放入集合后找不到自己 |
| 对称性 | x.equals(y) == y.equals(x) |
集合行为取决于调用顺序 |
| 传递性 | x=y 且 y=z → x=z | 等价关系链断裂 |
| 一致性 | 多次调用结果不变(只要字段没变) | 集合中的对象时有时无 |
| 非空性 | x.equals(null) 必须返回 false |
空指针异常 |
标准的 equals 模板
@Override
public boolean equals(Object o) {
// 1. 自反性 + 性能优化:同一个对象直接返回 true
if (this == o) return true;
// 2. 非空性 + 类型检查
if (o == null || getClass() != o.getClass()) return false;
// 3. 强制转换后逐字段比较
Person person = (Person) o;
return age == person.age && Objects.equals(name, person.name);
}
为什么用 getClass() 而不是 instanceof?因为 instanceof 在继承时会破坏对称性:如果 ColorPoint extends Point,那 point.equals(colorPoint) 可能为 true,但 colorPoint.equals(point) 为 false(因为 point 不是 ColorPoint)。
hashCode 的约定
hashCode() 返回一个 int 值,用于在哈希表(HashMap、HashSet)中快速定位对象。它和 equals() 存在一个铁律:
如果两个对象
equals()相等,那么它们的hashCode()必须相同。
反过来不要求——两个 hashCode() 相同的对象不一定 equals() 相等(这叫哈希碰撞,是正常现象)。
为什么必须一起重写
想象一个 HashSet:当你执行 set.add(obj) 时,HashSet 先用 hashCode() 算出桶的位置,再用 equals() 判断桶里是否已有相同的元素。
obj.hashCode() → 桶编号 → 在桶内用 equals() 逐个比较
如果你重写了 equals() 但没有重写 hashCode(),两个逻辑相等的对象可能被分到不同的桶,equals() 连比较的机会都没有:
// 只重写了 equals,没重写 hashCode
Person p1 = new Person("Alice", 25);
Person p2 = new Person("Alice", 25);
Set<Person> set = new HashSet<>();
set.add(p1);
set.contains(p2); // false! ← p2 被分到了不同的桶
这就像总机转接电话——如果两个人报了不同的分机号(hashCode 不同),总机根本不会把他们转到同一个人那里去确认是否是同一位来电者(equals)。
hashCode 的实现方式
@Override
public int hashCode() {
return Objects.hash(name, age);
}
Objects.hash() 内部使用了经典的 31 因子散列:
// Arrays.hashCode 的简化逻辑
int result = 1;
for (Object element : a) {
result = 31 * result + (element == null ? 0 : element.hashCode());
}
return result;
为什么是 31?因为 31 是奇素数,31 * i 可以被 JVM 优化为 (i << 5) - i(位移比乘法快),同时散列分布效果好。
对象克隆
Java 提供了 Object.clone() 方法来创建对象的副本。但 clone() 的设计在 Java 社区中饱受争议(Joshua Blerta 在《Effective Java》中称之为"broken"),理解它的问题有助于选择更好的替代方案。
clone 的使用条件
- 类必须实现
Cloneable接口(一个空的标记接口) - 重写
clone()方法,将访问权限改为public - 在方法内调用
super.clone()
public class Person implements Cloneable {
private String name;
private Address address; // 引用类型字段
@Override
public Person clone() throws CloneNotSupportedException {
return (Person) super.clone();
}
}
浅拷贝与深拷贝
Object.clone() 默认执行的是浅拷贝(Shallow Copy):基本类型字段按值复制,引用类型字段只复制引用本身,不复制引用所指向的对象。
浅拷贝后的内存布局:
原始对象 person1 克隆对象 person2
┌──────────────────┐ ┌──────────────────┐
│ name ─────────┐ │ │ name ─────────┐ │
│ address ────┐ │ │ │ address ────┐ │ │
└─────────────┼─┼──┘ └─────────────┼─┼──┘
│ │ │ │
│ └──→ String "Alice" ←───┘ │ ← 共享!但 String 不可变,无法修改,所以安全
│ │
└─────→ Address 对象 ←───────┘ ← 共享!可变对象,修改一个影响另一个
注意:name 是 String 类型,属于引用类型而非基本类型。浅拷贝后两个对象的 name 字段指向同一个 String 对象。但由于 String 是不可变的(参见上一篇文章),不存在「通过一方修改影响另一方」的风险,所以不需要深拷贝。而 address 是可变对象,共享同一个引用就会导致问题。
**深拷贝(Deep Copy)**则会递归地复制所有引用类型字段指向的对象,确保两个对象完全独立:
@Override
public Person clone() throws CloneNotSupportedException {
Person cloned = (Person) super.clone();
cloned.address = this.address.clone(); // 手动深拷贝引用字段
return cloned;
}
更好的替代方案
由于 clone() 的诸多问题(必须实现标记接口、checked exception、浅拷贝陷阱),实践中更推荐:
| 方案 | 优点 | 适用场景 |
|---|---|---|
| 拷贝构造器 | 明确可控,不依赖 Cloneable | 通用 |
| 静态工厂方法 | 可以命名为 copyOf(),语义更清晰 |
不变对象构建 |
| 序列化/反序列化 | 自动处理整个对象图 | 对象结构复杂 |
拷贝构造器示例:
public Person(Person other) {
this.name = other.name;
this.address = new Address(other.address); // 深拷贝
}
小结
| 概念 | 核心要点 |
|---|---|
== vs equals |
== 比较地址,equals 比较内容(需重写) |
| equals 五约定 | 自反、对称、传递、一致、非空 |
| hashCode 铁律 | equals 相等 → hashCode 必须相同 |
| 浅拷贝 | 引用字段共享,修改一个影响另一个 |
| 深拷贝 | 递归复制,完全独立 |
| 最佳实践 | 优先使用拷贝构造器替代 clone() |