Object 类:万类之祖
本章定位:Object 是所有 Java 类的终极父类。它的 equals()、hashCode()、toString() 三大方法构成了 Java 对象模型的基石。理解 equals/hashCode 契约,是使用 HashMap、HashSet 等集合类的前提,也是面试中的"必考题"。
小崔的 HashMap 找不到数据之谜
小崔在飞翔科技的员工管理系统中遇到了一个诡异的 bug:
Map<Employee, String> departmentMap = new HashMap<>();
Employee emp1 = new Employee(1001, "小崔");
Employee emp2 = new Employee(1001, "小崔");
departmentMap.put(emp1, "技术部");
String dept = departmentMap.get(emp2); // 用相同 ID 和姓名的另一个对象查询
System.out.println(dept); // null !!! 为什么找不到?
"明明 ID 和姓名都一样,为什么 HashMap 认为它们不同?"小崔抓耳挠腮。
白歌在 Code Review 时一针见血:"你没重写 equals() 和 hashCode()。"
Object 类概述
java.lang.Object 是所有 Java 类的直接或间接父类。如果一个类没有显式 extends 任何类,编译器会自动让它继承 Object。
三大核心方法详解
toString() 方法
默认实现(Object 类):
public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
// 例如:com.feixiang.Employee@1a2b3c4d
重写原则:返回有意义的对象描述信息。
@Override
public String toString() {
return "Employee{id=" + id + ", name='" + name + "', salary=" + salary + "}";
}
equals() 方法
默认实现(Object 类):
public boolean equals(Object obj) {
return (this == obj); // 仅比较地址
}
重写 equals() 的五条军规:
| 规则 | 含义 | 违反后果 |
|---|---|---|
| 自反性 | x.equals(x) 必须为 true | 集合查找自身失败 |
| 对称性 | x.equals(y) == y.equals(x) | 集合行为不确定 |
| 传递性 | x.equals(y) && y.equals(z) → x.equals(z) | 排序/比较混乱 |
| 一致性 | 多次调用结果相同(除非被修改) | 集合操作不可预测 |
| 非空性 | x.equals(null) 必须为 false | NPE 或错误结果 |
hashCode() 方法
默认实现(JDK 8 HotSpot):
// Object 类声明
public native int hashCode();
// 默认根据对象内存地址生成哈希值
equals() 与 hashCode() 的黄金契约:
核心记忆:相等的对象必须有相等的哈希码;哈希码相等的对象不一定相等。
完整示例一:重写 equals() 和 hashCode()
场景
小崔为飞翔科技的 Employee 类重写 equals() 和 hashCode(),使相同 ID 的员工被视为同一对象。
代码
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
class Employee {
private int id;
private String name;
private double salary;
public Employee(int id, String name, double salary) {
this.id = id;
this.name = name;
this.salary = salary;
}
// ========== 重写 equals() ==========
@Override
public boolean equals(Object o) {
// 1. 自反性检查
if (this == o) return true;
// 2. null 检查和类型检查
if (o == null || getClass() != o.getClass()) return false;
// 3. 强制转换 + 关键字段比较
Employee employee = (Employee) o;
return id == employee.id; // 飞翔科技规定:ID 相同即同一员工
}
// ========== 重写 hashCode() ==========
@Override
public int hashCode() {
return Objects.hash(id); // 与 equals 使用相同字段
}
// ========== 重写 toString() ==========
@Override
public String toString() {
return "Employee{id=" + id + ", name='" + name + "', salary=" + salary + "}";
}
}
public class EmployeeMapDemo {
public static void main(String[] args) {
Employee emp1 = new Employee(1001, "小崔", 15000);
Employee emp2 = new Employee(1001, "小崔", 15000); // 相同 ID
Employee emp3 = new Employee(1002, "白歌", 30000);
// ========== equals 测试 ==========
System.out.println("emp1.equals(emp2): " + emp1.equals(emp2)); // true
System.out.println("emp1.equals(emp3): " + emp1.equals(emp3)); // false
// ========== hashCode 测试 ==========
System.out.println("emp1.hashCode(): " + emp1.hashCode());
System.out.println("emp2.hashCode(): " + emp2.hashCode()); // 与 emp1 相同
System.out.println("emp3.hashCode(): " + emp3.hashCode()); // 不同
// ========== HashMap 测试 ==========
Map<Employee, String> departmentMap = new HashMap<>();
departmentMap.put(emp1, "技术部");
// 用 emp2(不同对象,但 equals 认为相等)查询
String dept = departmentMap.get(emp2);
System.out.println("emp2 所在部门: " + dept); // 技术部(不再是 null!)
// ========== toString 测试 ==========
System.out.println(emp1); // Employee{id=1001, name='小崔', salary=15000.0}
}
}
运行输出
emp1.equals(emp2): true
emp1.equals(emp3): false
emp1.hashCode(): 1001
emp2.hashCode(): 1001
emp3.hashCode(): 1002
emp2 所在部门: 技术部
Employee{id=1001, name='小崔', salary=15000.0}
HashMap 查找流程(equals/hashCode 协作)
关键点:HashMap 先通过
hashCode()定位桶,再用equals()在桶内精确匹配。如果只重写equals()不重写hashCode(),两个相等的对象可能被分到不同的桶,导致永远找不到。
clone() 方法与 Cloneable 接口
基本概念
| 术语 | 说明 |
|---|---|
| 浅拷贝(Shallow Copy) | 复制对象本身,但引用类型字段仍指向原对象的内存 |
| 深拷贝(Deep Copy) | 复制对象本身及其所有引用类型字段指向的对象(递归) |
使用 clone()
class Employee implements Cloneable { // 必须实现 Cloneable
private int id;
private String name;
private Department dept; // 引用类型
@Override
public Employee clone() {
try {
Employee cloned = (Employee) super.clone(); // 浅拷贝
// 如果需要深拷贝:
cloned.dept = this.dept.clone(); // 手动拷贝引用字段
return cloned;
} catch (CloneNotSupportedException e) {
throw new AssertionError(); // 不应该发生
}
}
}
注意:
clone()是protected native方法,必须实现Cloneable接口并重写为public。
完整示例二:浅拷贝与深拷贝对比
代码
class Department implements Cloneable {
private String name;
public Department(String name) { this.name = name; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
@Override
public Department clone() {
try {
return (Department) super.clone();
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
@Override
public String toString() { return "Department{name='" + name + "'}"; }
}
class EmployeeV2 implements Cloneable {
private int id;
private String name;
private Department department; // 引用类型字段
public EmployeeV2(int id, String name, Department department) {
this.id = id;
this.name = name;
this.department = department;
}
// 浅拷贝
public EmployeeV2 shallowClone() {
try {
return (EmployeeV2) super.clone();
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
// 深拷贝
public EmployeeV2 deepClone() {
try {
EmployeeV2 cloned = (EmployeeV2) super.clone();
cloned.department = this.department.clone(); // 手动拷贝引用字段
return cloned;
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
public Department getDepartment() { return department; }
@Override
public String toString() {
return "EmployeeV2{id=" + id + ", name='" + name + "', dept=" + department + "}";
}
}
public class CloneDemo {
public static void main(String[] args) {
Department techDept = new Department("技术部");
EmployeeV2 emp1 = new EmployeeV2(1001, "小崔", techDept);
// ========== 浅拷贝 ==========
EmployeeV2 shallow = emp1.shallowClone();
System.out.println("--- 浅拷贝 ---");
System.out.println("emp1 == shallow: " + (emp1 == shallow)); // false
System.out.println("emp1.dept == shallow.dept: "
+ (emp1.getDepartment() == shallow.getDepartment())); // true! 同一个引用
// 修改原对象部门名称
techDept.setName("产品部");
System.out.println("修改原部门后 shallow 的部门: "
+ shallow.getDepartment().getName()); // 产品部(被影响了!)
// ========== 深拷贝 ==========
Department techDept2 = new Department("技术部");
EmployeeV2 emp2 = new EmployeeV2(1002, "白歌", techDept2);
EmployeeV2 deep = emp2.deepClone();
System.out.println("\n--- 深拷贝 ---");
System.out.println("emp2.dept == deep.dept: "
+ (emp2.getDepartment() == deep.getDepartment())); // false! 不同引用
techDept2.setName("市场部");
System.out.println("修改原部门后 deep 的部门: "
+ deep.getDepartment().getName()); // 技术部(不受影响!)
}
}
运行输出
--- 浅拷贝 ---
emp1 == shallow: false
emp1.dept == shallow.dept: true
修改原部门后 shallow 的部门: 产品部
--- 深拷贝 ---
emp2.dept == deep.dept: false
修改原部门后 deep 的部门: 技术部
getClass() 方法
public final native Class<?> getClass();
返回对象的运行时类型。常用于反射、日志、equals() 中的类型判断。
Employee emp = new Employee(1001, "小崔", 15000);
System.out.println(emp.getClass()); // class com.feixiang.Employee
System.out.println(emp.getClass().getName()); // com.feixiang.Employee
System.out.println(emp.getClass().getSimpleName()); // Employee
getClass()是final方法,无法被重写,返回的是实际运行时类型。
finalize() 方法(已废弃)
@Deprecated(since="9") // JDK 9 起废弃
protected void finalize() throws Throwable { }
finalize() 在 GC 回收对象前被调用,本意是给对象"临终清理"的机会。但存在严重问题:
- 调用时机不确定:不知道何时执行,甚至永远不执行
- 性能开销大:有 finalize 的对象 GC 时需要两次回收
- 可能导致对象"复活":finalize 中将
this重新赋值给静态引用
替代方案:使用
try-with-resources(实现AutoCloseable接口)或Cleaner(JDK 9+)。
易错场景
❌ 反例一:重写 equals 不重写 hashCode
// ❌ 错误:equals 用 ID 判断,hashCode 用默认地址
class Employee {
private int id;
@Override
public boolean equals(Object o) {
if (o instanceof Employee) {
return this.id == ((Employee) o).id;
}
return false;
}
// 没有重写 hashCode()!
}
后果:new Employee(1001) 和 new Employee(1001) 虽然 equals 为 true,但 hashCode 不同(默认基于地址),放入 HashMap 后无法用另一个对象找回。
纠正:
@Override
public int hashCode() {
return Objects.hash(id); // 与 equals 使用相同的关键字段
}
❌ 反例二:equals 参数类型错误
// ❌ 错误:参数是 Employee 不是 Object——这是重载,不是重写!
public boolean equals(Employee obj) {
return this.id == obj.id;
}
当调用
emp.equals((Object) otherEmp)时,仍会调用 Object 的equals方法(比较地址)。必须用@Override注解让编译器帮你检查。
纠正:
@Override // 加上这个注解,编译器会检查
public boolean equals(Object obj) {
if (this == obj) return true;
if (!(obj instanceof Employee)) return false;
Employee other = (Employee) obj;
return this.id == other.id;
}
❌ 反例三:用可变字段参与 hashCode
// ❌ 错误:name 可变,对象放入 HashMap 后修改 name 导致 hash 变化
class Employee {
private int id;
private String name;
@Override
public int hashCode() {
return Objects.hash(id, name); // name 可变!
}
}
后果:对象放入 HashSet 后修改了 name,导致无法 remove 也无法找到。
纠正:只用不可变字段(如 ID)参与 hashCode 计算,或确保对象放入集合后不修改参与 hash 的字段。
面试考点
Q1:为什么重写 equals() 必须重写 hashCode()?
这是 HashMap、HashSet 等哈希数据结构正确工作的前提。HashMap 先用
hashCode()定位桶,再用equals()在桶内精确匹配。如果两个对象equals为 true 但hashCode不同,它们会被分到不同桶,导致map.get(b)找不到map.put(a)存的值。这违反了 HashMap 的约定:相等的对象应该在同一个桶中。
Q2:equals() 重写的五个原则是什么?
① 自反性:
x.equals(x)= true;② 对称性:x.equals(y)=y.equals(x);③ 传递性:x.equals(y) && y.equals(z)→x.equals(z);④ 一致性:多次调用结果一致;⑤ 与 null 比较:x.equals(null)= false。违反任一原则都会导致集合类(List、Set、Map)的行为不可预测。
Q3:浅拷贝和深拷贝的区别?如何实现深拷贝?
浅拷贝只复制对象本身的字段,引用类型字段指向与原对象相同的内存;深拷贝递归复制所有引用类型字段指向的对象。实现方式:① 重写
clone()并手动拷贝每个引用字段;② 通过序列化/反序列化(ObjectOutputStream/ObjectInputStream);③ 使用拷贝构造器或静态工厂方法。实际项目中推荐方案③,避免clone()的诸多陷阱。
Q4:== 和 equals() 有什么区别?
==比较的是栈中存储的值:基本类型比较数值,引用类型比较内存地址。equals()是 Object 定义的方法,默认实现就是==(比较地址),但 String、Integer 等类重写了它以比较内容。自定义类需要手动重写equals()来实现业务语义上的相等判断。
Q5:getClass() 和 instanceof 在 equals() 中的选择?
getClass()要求类型严格一致(子类对象与父类对象 equals 永远为 false);instanceof允许子类对象与父类对象比较。选择取决于业务语义:如果 Employee 和其子类 Manager 应该能通过 ID 判等,用instanceof;如果要求严格类型相同,用getClass()。但使用instanceof时要注意对称性——确保两个类都正确实现了 equals。
白歌说:"Object 的三大方法——toString、equals、hashCode——是我面试时必问的三板斧。能把这三点讲透的人,面向对象基本功一定扎实。而 clone 和 finalize,了解即可,实际项目中有更好的替代方案。"