Object通用方法
核心概念
Object 类是java中的根类,提供了面向对象编程的基础能力,所有类都隐式继承自 Object 类。
在Object 类中提供了一些重要的方法,支持对象的比较、哈希码生成、对象状态的表示、线程间通信等功能,其中除了线程同步的方法外,都可以被重写。
理解这些方法的设计原理和正确使用方式,是Java开发的基础。
public class Object {
// 比较相关
public boolean equals(Object obj)
public native int hashCode()
// 字符串表示
public String toString()
// 对象复制
protected native Object clone() throws CloneNotSupportedException
// 线程通信
public final native void wait() throws InterruptedException
public final native void notify()
public final native void notifyAll()
// 垃圾回收(已废弃)
protected void finalize() throws Throwable
// 反射相关
public final native Class<?> getClass()
}
一、equals()方法
用于比较两个对象是否“等价”,这个等价:`equals()` 与 \=\= 的区别主要在于对引用类型的判断逻辑不一样,\=\= 是比较两个引用是不是指向同一个对象,也就是“是不是一个地址”,而 `equals()` 是比较两个对象的内容是否相等,比如两个 `String` 内容一样,`equals()` 是 true,但 \=\= 可能是 false。
1.1 默认实现
// Object类的默认实现:比较引用地址
public boolean equals(Object obj) {
return (this == obj);
}
1.2 equals的5大约定(规范)
我们自己写的类如果想要通过 `equals()` 正确比较内容,就需要重写这个方法,并且要遵守一套规则——也就是“equals 通用约定”,包括五个特性:自反性(`x.equals(x)` 一定为 true)、对称性(`x.equals(y)` 与 `y.equals(x)` 结果相同)、 传递性(若 `x.equals(y)` 且 `y.equals(z)`,则 `x.equals(z)`)、 一致性(多次比较结果不变)、非空性(任何对象 `x` 调用 `x.equals(null)` 都应返回 false)。
| 约定 | 说明 | 示例 |
|---|---|---|
| 自反性 | x.equals(x) 必须为true | a.equals(a) → true |
| 对称性 | x.equals(y) = y.equals(x) | a.equals(b) = b.equals(a) |
| 传递性 | x=y, y=z → x=z | a=b, b=c → a=c |
| 一致性 | 多次调用结果一致 | 对象不变,结果不变 |
| 非空性 | x.equals(null) 必须为false | a.equals(null) → false |
1.3 标准重写模板
为了满足这些特性,我们一般遵循一个模板:先判断两个对象是不是同一个引用,如果是就返回 true;再判断类是否相同;然后强转类型,最后逐个比较字段内容。
public class User {
private Long id;
private String name;
private Integer age;
@Override
public boolean equals(Object o) {
// 1. 检查是否同一对象(性能优化)
if (this == o) return true;
// 2. 检查null和类型
if (o == null || getClass() != o.getClass()) return false;
// 3. 类型转换
User user = (User) o;
// 4. 比较关键字段
return Objects.equals(id, user.id) &&
Objects.equals(name, user.name) &&
Objects.equals(age, user.age);
}
}
1.4 常见错误
// ❌ 错误1:参数类型写错(重载而非重写)
public boolean equals(User other) { // 应该是Object
return this.id.equals(other.id);
}
// ❌ 错误2:没有处理null
public boolean equals(Object o) {
User user = (User) o; // o为null时NPE
return this.id.equals(user.id);
}
// ❌ 错误3:使用instanceof导致对称性问题
// 子类和父类比较时可能不对称
public boolean equals(Object o) {
if (!(o instanceof User)) return false; // 子类也会通过
// ...
}
1.5 getClass() vs instanceof
getClass()
getClass() 是 Object 类中的 final 方法,不可被重写,用于获取对象的实际运行时类型,返回一个 Class 类型对象,常用于类型判断或反射操作。
它和 instanceof 的区别在于判断粒度不同:
instanceof判断“是否属于某类或其子类”,适合继承结构中做兼容性判断;getClass()判断的是“是否完全属于同一个类”,适用于精确类型匹配,常用于equals()的实现中避免子类干扰。
// getClass() 精确匹配
Object obj = new ArrayList<>();
System.out.println(obj.getClass() == ArrayList.class); // true ✅
System.out.println(obj.getClass() == List.class); // false ❌
// instanceof 检查继承关系
System.out.println(obj instanceof ArrayList); // true ✅
System.out.println(obj instanceof List); // true ✅
System.out.println(obj instanceof Collection); // true ✅
// getClass():运行时类型,精确匹配
// instanceof:检查 is-a 关系,包括子类
// 陷阱:null 的处理
Object nullObj = null;
// System.out.println(nullObj.getClass()); // NPE ❌
System.out.println(nullObj instanceof Object); // false ✅
Object o = new ArrayList<>();
System.out.println(o instanceof List); // true
System.out.println(o.getClass() == List.class); // false
选择建议:
- 一般情况用
getClass(),保证对称性 - 如果需要子类与父类相等,用
instanceof(需谨慎设计)
二、hashCode()方法
hashCode()用于返回哈希值,为哈希表(如 HashMap)提供快速索引,将对象映射到 “桶位置”。等价的两个对象散列值一定相同,但是散列值相同的两个对象不一定等价,这是因为计算哈希值具有随机性,两个值不同的对象可能计算出相同的哈希值。所以在重写 equals() 方法时应当总是重写 hashCode() 方法,保证等价的两个对象哈希值也相等。
2.1 默认实现
// Object类的默认实现:返回对象内存地址的哈希值
public native int hashCode();
2.2 hashCode的3大约定
| 约定 | 说明 |
|---|---|
| 一致性 | 对象不变,hashCode不变 |
| equals相等→hashCode相等 | a.equals(b)=true → a.hashCode()=b.hashCode() |
| hashCode相等≠equals相等 | hashCode相等不要求equals相等(允许哈希冲突) |
2.3 为什么重写equals必须重写hashCode?
如果重写了 `equals()`,也必须重写 `hashCode()`,原因是 Java 中的集合类,比如 `HashMap`、`HashSet`,查找元素时是先用 `hashCode()` 找“桶位置”,再用 `equals()` 判断是否是“同一个”。如果两个对象 `equals()` 相等而 `hashCode()` 不一致,在 HashMap 这类基于哈希的结构中,它们会被分到不同的桶,导致找不到或插入重复,破坏集合的正确性。
// 只重写equals,不重写hashCode
public class User {
private Long id;
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
User user = (User) o;
return Objects.equals(id, user.id);
}
// 没有重写hashCode
}
// 问题:HashMap无法正常工作
User u1 = new User(1L);
User u2 = new User(1L);
System.out.println(u1.equals(u2)); // true
Map<User, String> map = new HashMap<>();
map.put(u1, "user1");
System.out.println(map.get(u2)); // null!而不是"user1"
// 原因:u1和u2的hashCode不同,HashMap先用hashCode定位桶
// hashCode不同,直接认为是不同的key
2.4 标准重写模板
至于如何重写`hashcode()`,目标就是:**相等的对象** `hashCode()` **必须相同,不相等的尽量分散,避免冲突**。最常见的写法是使用字符串哈希算法,将所有参与比较的域转换成R进制的数,R一般取31,因为31 是个奇质数,分布更均匀,且与 2 的幂不整除,可以更好地打散哈希值。此外,`31*x` 在底层可以被 JVM 优化为 `(x << 5) - x`,效率更优。
@Override
public int hashCode() {
return Objects.hash(id, name, age);
}
// 或者手动实现(性能更好)
@Override
public int hashCode() {
int result = 17; // 非零初始值
result = 31 * result + (id != null ? id.hashCode() : 0);
result = 31 * result + (name != null ? name.hashCode() : 0);
result = 31 * result + (age != null ? age.hashCode() : 0);
return result;
}
2.5 为什么用31作为乘数?
result = 31 * result + fieldHashCode;
- 31是奇素数:减少哈希冲突
- 31 = 32 - 1 = 2^5 - 1:可优化为位运算
(result << 5) - result - JVM会自动优化:性能好
三、equals与hashCode的关系
3.1 核心关系图
equals相等 ──必须──→ hashCode相等
↑ ↓
└──── 不一定 ←───────┘
3.2 HashMap中的应用
// HashMap查找过程
1. 计算key的hashCode
2. 根据hashCode定位桶(数组下标)
3. 在桶内遍历链表/红黑树,用equals比较
// 如果只重写equals不重写hashCode
put(u1, "v1") → hashCode=100 → 桶[100]
get(u2) → hashCode=200 → 桶[200] → 找不到!
// 正确情况
put(u1, "v1") → hashCode=100 → 桶[100]
get(u2) → hashCode=100 → 桶[100] → equals比较 → 找到!
3.3 完整示例
public class User {
private Long id;
private String name;
private Integer age;
// 构造方法、getter/setter省略
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
User user = (User) o;
return Objects.equals(id, user.id) &&
Objects.equals(name, user.name) &&
Objects.equals(age, user.age);
}
@Override
public int hashCode() {
return Objects.hash(id, name, age);
}
}
// 使用
User u1 = new User(1L, "Tom", 20);
User u2 = new User(1L, "Tom", 20);
System.out.println(u1.equals(u2)); // true
System.out.println(u1.hashCode() == u2.hashCode()); // true
Set<User> set = new HashSet<>();
set.add(u1);
set.add(u2);
System.out.println(set.size()); // 1(正确去重)
四、toString()方法
用于返回对象的字符串表示形式,在打印对象、拼日志时都会被自动调用,比如 `System.out.println(obj)` 背后其实就是调用了 `obj.toString()`。
4.1 默认实现
如果没有重写,默认返回的是 `类名@对象哈希码`(哈希码是 `hashCode()` 的十六进制形式),如 `User@5f184fc6`,没有实际业务意义。
// Object类的默认实现
public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
// 示例
User user = new User();
System.out.println(user.toString()); // com.example.User@1b6d3586
4.2 重写规范
因此通常建议我们**重写该方法**,返回更有价值的信息:
@Override
public String toString() {
return "User{" +
"id=" + id +
", name='" + name + '\'' +
", age=" + age +
'}';
}
// 输出:User{id=1, name='Tom', age=20}
这样在调试、日志输出、打印对象信息时就能更直观地看到内容。
- 很多日志框架(如 SLF4J)和工具方法(如
Arrays.toString())依赖对象的toString(); - 使用 Lombok 时,
@ToString注解可自动生成该方法; - 覆盖
toString()时,注意避免输出敏感信息(如密码等); - 复杂对象推荐使用
StringJoiner或StringBuilder拼接,提高效率; - 可以使用
Objects.toString(field)来避免null。
4.3 使用工具类
// 方式1:IDE自动生成(推荐)
// 方式2:Lombok
@ToString
public class User {
private Long id;
private String name;
}
// 方式3:Apache Commons
@Override
public String toString() {
return ToStringBuilder.reflectionToString(this);
}
// 方式4:Google Guava
@Override
public String toString() {
return MoreObjects.toStringHelper(this)
.add("id", id)
.add("name", name)
.toString();
}
4.4 日志应用
// 日志输出时自动调用toString()
log.info("用户信息: {}", user); // 自动调用user.toString()
// 敏感信息处理
@Override
public String toString() {
return "User{" +
"id=" + id +
", name='" + name + '\'' +
", password='******'" + // 隐藏敏感信息
'}';
}
五、clone()方法
5.1 默认实现
// Object类的默认实现:native方法,浅拷贝
protected native Object clone() throws CloneNotSupportedException;
5.2 使用clone的步骤
// 1. 实现Cloneable接口(标记接口)
public class User implements Cloneable {
private Long id;
private String name;
private Address address; // 引用类型
// 2. 重写clone方法
@Override
public User clone() {
try {
return (User) super.clone();
} catch (CloneNotSupportedException e) {
throw new AssertionError(); // 不会发生
}
}
}
5.3 浅拷贝 vs 深拷贝
// 浅拷贝:只复制对象本身,引用类型共享
User u1 = new User(1L, "Tom", new Address("Beijing"));
User u2 = u1.clone();
System.out.println(u1 == u2); // false(不同对象)
System.out.println(u1.getAddress() == u2.getAddress()); // true(共享引用)
u2.getAddress().setCity("Shanghai");
System.out.println(u1.getAddress().getCity()); // Shanghai(被影响)
// 深拷贝:递归复制所有引用对象
@Override
public User clone() {
try {
User cloned = (User) super.clone();
cloned.address = this.address.clone(); // 深拷贝引用对象
return cloned;
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
5.4 为什么不推荐使用clone()?
| 问题 | 说明 |
|---|---|
| 设计缺陷 | 需要实现Cloneable接口,但接口没有clone方法 |
| 浅拷贝陷阱 | 默认浅拷贝,容易出错 |
| 构造函数绕过 | clone不调用构造函数,可能破坏不变性 |
| 异常处理 | CloneNotSupportedException是受检异常 |
5.5 推荐的替代方案
// 方案1:拷贝构造函数(推荐)
public class User {
public User(User other) {
this.id = other.id;
this.name = other.name;
this.address = new Address(other.address); // 深拷贝
}
}
User u2 = new User(u1);
// 方案2:静态工厂方法
public static User copy(User other) {
User user = new User();
user.id = other.id;
user.name = other.name;
user.address = new Address(other.address);
return user;
}
User u2 = User.copy(u1);
// 方案3:序列化(深拷贝,性能较差)
public static <T extends Serializable> T deepCopy(T obj) {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(obj);
ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bis);
return (T) ois.readObject();
}
六、wait/notify/notifyAll方法
6.1 为什么在Object类中?
- 这些方法操作的是对象的监视器锁(Monitor)
- 每个对象都有一个监视器锁
- 所以定义在Object类中,所有对象都能使用
6.2 基本用法
synchronized (lock) {
while (!condition) {
lock.wait(); // 释放锁,进入等待状态
}
// 条件满足,继续执行
}
synchronized (lock) {
condition = true;
lock.notify(); // 唤醒一个等待线程
// 或 lock.notifyAll(); // 唤醒所有等待线程
}
6.3 生产者-消费者示例
public class Buffer {
private Queue<Integer> queue = new LinkedList<>();
private int capacity = 10;
public synchronized void produce(int value) throws InterruptedException {
while (queue.size() == capacity) {
wait(); // 队列满,等待消费
}
queue.offer(value);
notifyAll(); // 通知消费者
}
public synchronized int consume() throws InterruptedException {
while (queue.isEmpty()) {
wait(); // 队列空,等待生产
}
int value = queue.poll();
notifyAll(); // 通知生产者
return value;
}
}
6.4 注意事项
// ❌ 错误:没有在synchronized块中调用
lock.wait(); // IllegalMonitorStateException
// ❌ 错误:用if而不是while
synchronized (lock) {
if (!condition) { // 应该用while
lock.wait();
}
// 可能被虚假唤醒,condition仍为false
}
// ✅ 正确:用while循环检查条件
synchronized (lock) {
while (!condition) {
lock.wait();
}
}
七、finalize()方法(已废弃)
7.1 设计初衷
// 在对象被GC回收前调用,用于释放资源
protected void finalize() throws Throwable {
// 清理资源
}
7.2 为什么废弃?
| 问题 | 说明 |
|---|---|
| 不确定性 | 不知道何时执行,甚至可能不执行 |
| 性能问题 | 有finalize的对象GC更慢 |
| 复活问题 | finalize中可以让对象复活,导致内存泄漏 |
| 异常吞没 | finalize中的异常会被忽略 |
7.3 替代方案
// 方案1:try-with-resources(推荐)
try (FileInputStream fis = new FileInputStream("file.txt")) {
// 使用资源
} // 自动关闭
// 方案2:Cleaner(JDK 9+)
public class Resource implements AutoCloseable {
private static final Cleaner cleaner = Cleaner.create();
private final Cleaner.Cleanable cleanable;
public Resource() {
cleanable = cleaner.register(this, () -> {
// 清理逻辑
});
}
@Override
public void close() {
cleanable.clean();
}
}
八、高频面试题
Q1: 为什么重写equals必须重写hashCode?
答:
- HashMap等集合先用hashCode定位桶,再用equals比较
- 如果只重写equals,两个相等的对象可能有不同的hashCode
- 导致HashMap无法正确工作(put和get定位到不同的桶)
Q2: equals方法的5大约定是什么?
答:
- 自反性:x.equals(x) = true
- 对称性:x.equals(y) = y.equals(x)
- 传递性:x=y, y=z → x=z
- 一致性:多次调用结果一致
- 非空性:x.equals(null) = false
Q3: hashCode相等,equals一定相等吗?
答:
- 不一定。hashCode相等可能是哈希冲突
- 但equals相等,hashCode必须相等
Q4: 为什么hashCode用31作为乘数?
答:
- 31是奇素数,减少哈希冲突
- 31 = 2^5 - 1,可优化为位运算
- JVM会自动优化
31 * i为(i << 5) - i
Q5: 浅拷贝和深拷贝的区别?
答:
- 浅拷贝:只复制对象本身,引用类型共享同一对象
- 深拷贝:递归复制所有引用对象,完全独立
Q6: 为什么不推荐使用clone()?
答:
- Cloneable接口设计有缺陷
- 默认浅拷贝容易出错
- 不调用构造函数,可能破坏不变性
- 推荐使用拷贝构造函数或静态工厂方法
Q7: wait()和notify()为什么在Object类中?
答:
- 这些方法操作对象的监视器锁
- 每个对象都有监视器锁
- 所以定义在Object类中,所有对象都能使用
Q8: wait()和sleep()的区别?
答:
| 特性 | wait() | sleep() |
|---|---|---|
| 所属类 | Object | Thread |
| 释放锁 | 是 | 否 |
| 唤醒方式 | notify/超时 | 超时 |
| 使用场景 | 线程通信 | 暂停执行 |
九、最佳实践
✅ 推荐做法
// 1. 使用IDE或Lombok生成equals/hashCode
@EqualsAndHashCode
public class User {
private Long id;
private String name;
}
// 2. 使用Objects工具类
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
User user = (User) o;
return Objects.equals(id, user.id);
}
@Override
public int hashCode() {
return Objects.hash(id, name);
}
// 3. 使用拷贝构造函数代替clone
public User(User other) {
this.id = other.id;
this.name = other.name;
}
// 4. 使用try-with-resources代替finalize
try (Resource r = new Resource()) {
// 使用资源
}
❌ 避免做法
// 1. 只重写equals不重写hashCode
@Override
public boolean equals(Object o) { ... }
// 缺少hashCode重写
// 2. equals参数类型写错
public boolean equals(User other) { ... } // 应该是Object
// 3. 使用clone()
User u2 = u1.clone(); // 不推荐
// 4. 使用finalize()
protected void finalize() { ... } // 已废弃
相关链接
记忆口诀
equals五约定,自反对称传递性
一致非空要牢记,hashCode必同行
重写equals必重写hash,HashMap才能正常跑
clone有坑不推荐,拷贝构造是首选
wait释放锁等待,notify唤醒要在synchronized
finalize已废弃,try-with-resources来替代
⬅️ 动态代理 🏠 00-Java ➡️ instanceof
💬 评论