网站公告

本次更新:没啥更新的

“钱塘江上潮信起,今日方知我是我”

Skip to content

Object ​

字数: 0 字 时长: 0 分钟

1. Object 类 ​

1.1 Object 类概述 ​

在 Java 中,经常会听到一句话:

万物皆对象。

更准确地说,Java 中除了基本数据类型之外,我们平时定义和使用的大多数类型最终都与 Object 类有关。

Object 类位于:

java
java.lang.Object

它是 Java 类继承体系中的根类。

如果一个类没有显式指定父类:

java
public class Person {
}

实际上可以理解为:

java
public class Person extends Object {
}

因此,我们自己定义的类都可以使用 Object 中提供的方法:

java
Person person = new Person();

System.out.println(person.toString());
System.out.println(person.hashCode());
System.out.println(person.getClass());

数组本身也是对象:

java
int[] array = {1, 2, 3};

System.out.println(array.getClass());
System.out.println(array.hashCode());
System.out.println(array.toString());

直接打印数组时,经常会看到类似:

text
[I@1b6d3586

这样的结果,也是因为数组对象使用了 Object 中默认的 toString() 实现。

Object 主要提供了下面这些方法:

text
getClass()

hashCode()
equals()

clone()

toString()

wait()
wait(long timeout)
wait(long timeout, int nanos)

notify()
notifyAll()

finalize()

大致可以分为六类:

image-20250302202708193

text
对象比较
├── equals()
└── hashCode()

对象拷贝
└── clone()

对象转字符串
└── toString()

线程等待与通知
├── wait()
├── notify()
└── notifyAll()

获取类型信息
└── getClass()

对象终结
└── finalize()

下面分别来看这些方法。

1.2 对象比较 ​

1.2.1 hashCode() ​

hashCode() 用于返回对象的哈希值。

Object 中的方法定义如下:

java
public native int hashCode();

native 表示该方法的具体实现并不是普通 Java 代码,而是由 JVM 提供底层实现。

需要注意:

如果两个对象通过 equals() 判断相等,那么它们的 hashCode() 必须相等。

也就是说:

text
equals() 相等
      ↓
hashCode() 必须相等

但是反过来并不一定成立:

text
hashCode() 相等
      ↓
两个对象不一定 equals()

因为不同对象可能发生哈希冲突。

如果一个类重写了 equals(),通常也应该同时重写 hashCode()。

例如:

java
@Override
public int hashCode() {
    return Objects.hash(name, age);
}

equals() 和 hashCode() 的关系在 HashMap、HashSet 等集合中尤其重要,后面的集合笔记还会详细介绍。

1.2.2 equals() ​

equals() 用于判断两个对象是否相等。

Object 中默认实现如下:

java
public boolean equals(Object obj) {
    return this == obj;
}

可以看到,Object.equals() 默认直接使用:

java
==

也就是说,如果子类没有重写 equals(),默认比较的是:

两个引用是否指向同一个对象。

例如:

java
Person person1 = new Person();
Person person2 = new Person();

System.out.println(person1 == person2);
System.out.println(person1.equals(person2));

由于两个对象是分别 new 出来的,如果 Person 没有重写 equals(),结果都是:

text
false
false

但是在实际开发中,我们经常希望按照对象中的字段判断两个对象是否“相等”。

例如有一个 Person4 类,我们规定:

姓名和年龄相同,就认为是同一个人。

这时就可以重写 equals():

java
public class Person4 {

    private String name;
    private int age;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    @Override
    public boolean equals(Object obj) {

        // 如果两个引用本身就是同一个对象
        if (this == obj) {
            return true;
        }

        // 判断 obj 是否为 Person4
        if (obj instanceof Person4) {

            Person4 p = (Person4) obj;

            // 根据 name 和 age 判断两个对象是否相等
            return Objects.equals(this.name, p.getName())
                    && this.age == p.getAge();
        }

        return false;
    }

    @Override
    public int hashCode() {
        return Objects.hash(name, age);
    }
}

这里不仅重写了 equals(),同时也重写了 hashCode(),保证两者的约定一致。

像 String、Integer 等常用类,也都重写了 equals(),因此它们通常比较的是对象中的实际内容,而不是单纯比较对象地址。

1.3 对象拷贝 clone() ​

Object 中提供了 clone() 方法,用于创建当前对象的副本:

java
protected native Object clone()
        throws CloneNotSupportedException;

clone() 是一个 native 方法。

默认情况下,Object.clone() 执行的是浅拷贝(Shallow Copy)。

需要注意的是:

一个类如果想正常调用 clone(),通常需要实现 Cloneable 接口。

例如:

java
public class Person implements Cloneable {

    private String name;
    private int age;

    @Override
    protected Object clone()
            throws CloneNotSupportedException {

        return super.clone();
    }
}

然后:

java
Person person1 = new Person();

// 创建 person1 的副本
Person person2 = (Person) person1.clone();

Cloneable 是一个比较特殊的标记接口,本身没有定义任何方法。

如果对象没有实现 Cloneable,直接调用 Object.clone(),会抛出:

text
CloneNotSupportedException

默认 clone() 只进行浅拷贝。

例如对象中存在引用类型字段:

text
Person
├── name
├── age
└── Address

浅拷贝后,两个 Person 对象中的 Address 引用仍然可能指向同一个对象。

深拷贝和浅拷贝后面如果涉及对象复制,可以再单独展开。

1.4 对象转字符串 toString() ​

toString() 用于返回对象的字符串表示。

Object 中默认实现如下:

java
public String toString() {
    return getClass().getName()
            + "@"
            + Integer.toHexString(hashCode());
}

因此,如果一个类没有重写 toString():

java
Person person = new Person();

System.out.println(person);

输出结果可能类似:

text
com.kerwim.Person@3d075dc0

其中:

text
com.kerwim.Person

表示对象所属的类。

而:

text
3d075dc0

是 hashCode() 转换成的十六进制字符串。

这样的结果通常没有太多业务意义,因此实际开发中经常会重写 toString()。

例如:

java
@Override
public String toString() {
    return "Person{" +
            "name='" + name + '\'' +
            ", age=" + age +
            '}';
}

这时打印:

java
System.out.println(person);

就可以得到类似:

text
Person{name='kerwim', age=18}

比默认输出直观很多。

这项工作也可以直接交给 IntelliJ IDEA。

例如:

text
Generate
    ↓
toString()

IDE 就可以自动生成对应代码。

现在很多项目还会使用 Lombok:

java
@Data
public class Person {

    private String name;

    private int age;
}

@Data 会自动生成:

text
getter
setter
equals()
hashCode()
toString()

等常见方法。

数组也是对象,因此直接打印数组时:

java
int[] array = {1, 2, 3};

System.out.println(array);

可能会得到:

text
[I@1b6d3586

因为数组没有像 String 那样重写 toString()。

所以打印数组内容通常应该使用:

java
System.out.println(Arrays.toString(array));

1.5 多线程调度 wait、notify、notifyAll ​

Object 还提供了一组和线程等待、通知有关的方法:

java
wait()

notify()

notifyAll()

为什么这些方法定义在 Object 中?

因为 Java 中的普通对象都可以作为:

text
锁对象
Monitor

用于线程之间的等待与通知。

这里先简单看一个例子:

java
public class WaitNotifyDemo {

    public static void main(String[] args) {

        // 创建一个锁对象
        Object lock = new Object();

        // 线程 1
        new Thread(() -> {

            synchronized (lock) {

                System.out.println("线程1:我要等待");

                try {

                    // 当前线程进入等待状态
                    lock.wait();

                } catch (InterruptedException e) {

                    // 恢复线程的中断状态
                    Thread.currentThread().interrupt();
                }

                System.out.println("线程1:我被唤醒了");
            }

        }).start();

        // 线程 2
        new Thread(() -> {

            synchronized (lock) {

                System.out.println("线程2:我要唤醒线程");

                // 唤醒一个等待在 lock 上的线程
                lock.notify();

                System.out.println("线程2:已经执行 notify()");
            }

        }).start();
    }
}

大致过程为:

text
线程 1
  ↓
获得 lock 锁
  ↓
lock.wait()
  ↓
进入等待状态
  ↓
释放 lock


线程 2
  ↓
获得 lock 锁
  ↓
lock.notify()
  ↓
通知等待线程


线程 1
  ↓
重新竞争 lock
  ↓
获得锁之后继续执行

这里特别需要注意:

wait()、notify()、notifyAll() 必须在当前线程已经持有对应对象 Monitor 的情况下调用。

所以通常要配合:

java
synchronized (lock)

使用。

wait() ​

java
public final void wait()
        throws InterruptedException

调用之后,当前线程会进入等待状态,直到:

text
其他线程调用 notify()

或者

其他线程调用 notifyAll()

或者

线程被中断

notify() ​

java
public final native void notify();

用于唤醒在这个对象 Monitor 上等待的一个线程。

如果有多个线程正在等待,具体哪个线程被唤醒并不由程序直接指定。

notifyAll() ​

java
public final native void notifyAll();

用于唤醒在这个对象 Monitor 上等待的所有线程。

这些线程被唤醒之后,还需要重新竞争锁。

wait(long timeout) ​

java
public final native void wait(long timeout)
        throws InterruptedException;

最长等待 timeout 毫秒。

例如:

java
lock.wait(1000);

最多等待:

text
1000 ms

wait(long timeout, int nanos) ​

java
public final void wait(
        long timeout,
        int nanos)
        throws InterruptedException

可以在毫秒的基础上再指定纳秒参数。

wait/notify 属于比较典型的线程通信机制,后面的 JUC 笔记中还会详细介绍。

1.6 获取类型信息 getClass() ​

Object 中还提供了:

java
public final native Class<?> getClass();

用于获取对象在运行时对应的 Class 对象。

例如:

java
public class GetClassDemo {

    public static void main(String[] args) {

        Person person = new Person();

        // 获取 person 在运行时对应的 Class 对象
        Class<? extends Person> clazz =
                person.getClass();

        // 获取完整类名
        System.out.println(clazz.getName());
    }
}

假设 Person 位于:

text
com.kerwim.ObjAndClass

那么输出结果:

text
com.kerwim.ObjAndClass.Person

还可以继续获取更多类的信息:

java
Person person = new Person();

Class<?> clazz = person.getClass();

// 完整类名
System.out.println(clazz.getName());

// 简单类名
System.out.println(clazz.getSimpleName());

// 当前 Class 对象
System.out.println(clazz);

getClass() 是 Java 反射机制的重要入口之一。

这里只先了解:

text
对象
 ↓
getClass()
 ↓
Class 对象
 ↓
获取类的运行时信息

反射相关内容会在 反射.md 中详细介绍。

1.7 垃圾回收 finalize() ​

Object 曾经提供:

java
protected void finalize()
        throws Throwable

这个方法最初的设计目的是:

当垃圾回收器准备回收一个对象时,可以在对象真正被回收之前执行一些清理操作。

例如以前可能看到这样的写法:

java
@Override
protected void finalize()
        throws Throwable {

    System.out.println("对象准备被回收");

    super.finalize();
}

但是 finalize() 存在很多问题:

text
什么时候执行无法确定

不保证一定会执行

会增加 GC 负担

可能导致对象回收延迟

容易产生资源泄漏等问题

因此,从 Java 9 开始:

text
finalize()

已经被标记为废弃。

现在的 Java 项目中:

不要再依赖 finalize() 释放资源。

例如文件、数据库连接、网络连接等资源,应该通过:

text
try-with-resources
AutoCloseable
close()

等更加明确的方式释放。

image-20250302203907356

所以对于 finalize(),现在主要知道它的历史作用即可:

text
早期 Java
    ↓
对象被 GC 前尝试执行清理逻辑


现代 Java
    ↓
finalize() 已废弃
    ↓
不推荐继续使用

具体的对象回收机制、垃圾回收器以及 GC,会在 JVM 部分详细介绍。

1.8 Object 方法总结 ​

最后把 Object 中比较重要的方法简单归纳一下:

方法作用
equals()判断两个对象是否相等
hashCode()获取对象的哈希值
toString()获取对象的字符串表示
clone()创建对象副本
getClass()获取对象运行时对应的 Class
wait()让当前线程进入等待状态
notify()唤醒一个等待线程
notifyAll()唤醒所有等待线程
finalize()对象终结方法,已经废弃

其中实际开发中最常见的通常是:

text
equals()
hashCode()
toString()
getClass()

而:

text
wait()
notify()
notifyAll()

主要会在线程通信和并发编程中遇到。

Object 可以看作 Java 对象体系的基础,几乎所有普通 Java 类最终都会继承它,因此理解 Object 中这些方法,对后面的集合、反射、并发以及 JVM 学习都会有帮助。

Noctiflorous Garden · MIT License