Object类与常用方法
Object 是 Java 类继承体系的根类。只要一个类没有显式继承其他类,就默认继承 Object。
public class User {
}
等价于:
public class User extends Object {
}
理解 Object 的方法,能帮助你掌握对象比较、哈希容器、日志输出、类型判断、克隆和线程协作等基础机制。
# 1. Object 在类型体系中的位置
Object
├─ String
├─ Number
│ ├─ Integer
│ ├─ Long
│ └─ Double
├─ Throwable
│ ├─ Exception
│ └─ Error
└─ 自定义类
└─ User
任何对象都可以赋值给 Object 引用:
Object value = "hello";
Object user = new User();
但 Object 引用只能直接调用 Object 定义的方法。如果需要调用子类方法,要进行类型判断和转换。
if (value instanceof String text) {
System.out.println(text.length());
}
# 2. Object 常用方法
| 方法 | 作用 |
|---|---|
toString() | 返回对象字符串表示 |
equals(Object obj) | 判断对象是否相等 |
hashCode() | 返回对象哈希值 |
getClass() | 返回运行时类对象 |
clone() | 克隆对象 |
wait() | 让当前线程等待 |
notify() | 唤醒一个等待线程 |
notifyAll() | 唤醒所有等待线程 |
finalize() | 对象回收前回调,已不推荐使用 |
其中 equals、hashCode、toString 是日常业务开发最常重写的方法。
# 3. toString
toString() 用于返回对象的文本表示。
默认实现通常类似:
类名@哈希值十六进制
示例:
User user = new User();
System.out.println(user); // com.example.User@1b6d3586
更有用的做法是重写:
public class User {
private Long id;
private String name;
@Override
public String toString() {
return "User{id=" + id + ", name='" + name + "'}";
}
}
开发建议:
- 日志中输出对象时,
toString应包含关键定位字段。 - 不要在
toString中输出密码、密钥、身份证等敏感信息。 - 不要在
toString中触发远程调用或复杂计算。
# 4. equals
Object 默认的 equals 与 == 类似,比较引用是否相同。
public boolean equals(Object obj) {
return (this == obj);
}
很多业务对象需要按业务字段判断相等。
public class User {
private Long id;
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (!(obj instanceof User other)) {
return false;
}
return Objects.equals(this.id, other.id);
}
}
equals 必须满足约定:
| 约定 | 含义 |
|---|---|
| 自反性 | x.equals(x) 必须为 true |
| 对称性 | x.equals(y) 与 y.equals(x) 结果一致 |
| 传递性 | x=y 且 y=z,则 x=z |
| 一致性 | 对象状态不变时,多次调用结果一致 |
| 非空性 | x.equals(null) 必须为 false |
# 5. hashCode
hashCode() 返回对象的哈希值,常用于哈希容器。
如果重写 equals,通常必须重写 hashCode。
规则:
| 规则 | 说明 |
|---|---|
equals 相等 | hashCode 必须相等 |
hashCode 相等 | equals 不一定相等 |
| 对象状态不变 | hashCode 应保持稳定 |
示例:
public class User {
private Long id;
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (!(obj instanceof User other)) {
return false;
}
return Objects.equals(id, other.id);
}
@Override
public int hashCode() {
return Objects.hash(id);
}
}
# 6. equals 和 hashCode 与 HashMap
HashMap 查找 key 的过程可以简化为:
put(key, value)
│
▼
计算 key.hashCode()
│
▼
定位桶位置
│
▼
如果桶中已有元素,用 equals 判断是否同一个 key
如果只重写 equals 不重写 hashCode,可能出现“明明相等却找不到”的问题。
Set<User> users = new HashSet<>();
users.add(new User(1L));
users.contains(new User(1L)); // 可能是 false
原因是两个相等对象的哈希值不同,被放到不同桶里。
关系图:
equals 相等
│
▼
hashCode 必须相等
│
▼
才能在 HashMap / HashSet 中稳定工作
# 7. getClass 与 instanceof
getClass() 返回对象运行时类型:
Object value = "hello";
System.out.println(value.getClass()); // class java.lang.String
instanceof 判断对象是否属于某个类型或其子类型:
if (value instanceof CharSequence) {
// String 实现了 CharSequence
}
在 equals 中,两种写法语义不同:
if (obj == null || getClass() != obj.getClass()) {
return false;
}
表示必须完全同类才相等。
if (!(obj instanceof User other)) {
return false;
}
表示子类对象也可能参与相等比较。
选择时要考虑继承关系。如果类不是为了继承设计,使用 getClass 更严格;如果希望父子类按统一业务字段比较,使用 instanceof。
# 8. clone
clone() 用于创建对象副本,但它有几个历史包袱:
Object.clone()是protected。- 类必须实现
Cloneable接口,否则可能抛CloneNotSupportedException。 - 默认是浅拷贝。
示例:
public class User implements Cloneable {
private String name;
@Override
public User clone() {
try {
return (User) super.clone();
} catch (CloneNotSupportedException e) {
throw new AssertionError(e);
}
}
}
浅拷贝:
原对象.address ─────┐
▼
Address
▲
副本.address ───────┘
如果对象内部有可变引用字段,浅拷贝后两个对象会共享内部对象。
实际开发中更推荐:
- 使用拷贝构造器。
- 使用静态工厂方法。
- 使用序列化框架或映射工具做明确复制。
public User(User source) {
this.id = source.id;
this.name = source.name;
this.address = new Address(source.address);
}
# 9. wait、notify、notifyAll
这三个方法用于线程间协作,必须在持有对象监视器锁时调用。
synchronized (lock) {
while (!ready) {
lock.wait();
}
}
唤醒:
synchronized (lock) {
ready = true;
lock.notifyAll();
}
等待唤醒模型:
线程 A 持有 lock
│ 条件不满足
▼
调用 wait,释放 lock,进入等待队列
线程 B 持有 lock
│ 修改条件
▼
调用 notifyAll,唤醒等待线程
线程 A 重新竞争 lock
│
▼
条件满足后继续执行
为什么用 while 而不是 if:
- 线程可能被虚假唤醒。
- 多个线程被唤醒后条件可能已被其他线程消费。
- 重新获得锁时必须再次检查条件。
现代并发开发中,更常使用 java.util.concurrent 包下的工具,例如 Lock、Condition、CountDownLatch、BlockingQueue。
# 10. finalize
finalize() 曾经用于对象被 GC 前的清理回调,但已经不推荐使用。
问题:
- 执行时机不确定。
- 可能造成对象复活。
- 影响 GC 性能。
- 容易导致资源释放不及时。
释放文件、网络连接、数据库连接等资源,应使用 try-with-resources 或显式关闭。
try (Connection connection = dataSource.getConnection()) {
// use connection
}
# 11. Lombok 与 Object 方法
项目中常用 Lombok 自动生成 toString、equals、hashCode。
@Data
public class UserDTO {
private Long id;
private String name;
}
注意:
- 实体类使用
@Data时,要确认equals/hashCode是否包含可变字段。 - 双向关联对象生成
toString可能递归。 - 敏感字段需要排除。
- JPA 实体的
equals/hashCode要谨慎设计。
# Tips 快问快答
Q:所有 Java 类都继承 Object 吗?
A:普通类最终都继承 Object。接口不继承 Object,但接口实现类仍然是对象。
Q:重写 equals 必须重写 hashCode 吗?
A:通常必须。否则哈希集合和哈希映射可能工作异常。
Q:hashCode 相同代表对象相等吗?
A:不代表。哈希冲突允许存在,还需要 equals 判断。
Q:toString 可以输出所有字段吗?
A:不一定。敏感字段、过大字段、可能递归的字段都应避免直接输出。
Q:getClass 和 instanceof 哪个更适合 equals?
A:取决于相等语义。需要严格同类用 getClass,允许子类型参与比较用 instanceof。
Q:clone 是深拷贝吗?
A:默认不是。Object.clone() 是浅拷贝。
Q:为什么 wait 必须在 synchronized 中调用?
A:因为调用线程必须持有对象监视器锁,才能释放锁并进入该对象等待队列。
Q:为什么等待条件要用 while 判断? A:防止虚假唤醒和条件被其他线程改变。
Q:finalize 能用来释放资源吗?
A:不推荐。它执行时机不确定,应使用 try-with-resources 或显式关闭。
Q:Object obj = "abc" 后能直接调用 length() 吗?
A:不能。编译期类型是 Object,只能调用 Object 方法;需要类型判断并转换为 String。