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() 时,注意避免输出敏感信息(如密码等);
  • 复杂对象推荐使用 StringJoinerStringBuilder 拼接,提高效率;
  • 可以使用 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大约定是什么?

答:

  1. 自反性:x.equals(x) = true
  2. 对称性:x.equals(y) = y.equals(x)
  3. 传递性:x=y, y=z → x=z
  4. 一致性:多次调用结果一致
  5. 非空性: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