掘金 后端 ( ) • 2024-06-29 16:43

Object 概述

Object 类是 Java 中所有类的父类,这个类中包含了若干方法,这也就意味着所有类都将继承这些方法。因此,掌握这个类的方法是非常必要的,毕竟所有类都能为你提供这些方法。

Object 类位于 java.base 模块下 java.lang.Object,其结构如下: Object 中的那些方法

其中 wait0 是私有方法,不用管。我把这些方法分为两类,一类是常用方法(如 hashCodeequals );一类是线程相关方法(如 waitnotify)。 下面就对这些方法一一说明,进而全面掌握这个类的所有方法。

常用方法

1. getClass() 方法

public final native Class<?> getClass();

返回这个类对应的 Class 对象。final 方法,子类无法覆写。Class 对象是 Java 反射中最重要的一个类。有关反射的内容可以查看这个文章:Java Reflection 反射使用 完全指南 - 掘金 (juejin.cn)

2. hashCode() 方法

public native int hashCode();

返回这个对象的哈希值。默认情况下,Object 是返回对象在堆内存中的地址。一般来说,如果重写了 equals 方法时,一般也会重写这个方法。 另外,当时你使用 HashMap 这种需要对象哈希值的集合的时候,Java 会自动调用这个方法,用以确定这个对象对应的值放到哪个位置。

3. equals(Object) 方法

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

这个方法用于判断当前对象是否与传入的 obj 对象相等。在 Object 中,就是使用 == 来进行判断,即判断两个对象在内存中的地址是否相同。但是一般情况下,子类常常需要覆写此方法,来对不同的类做不同的相等判断。

重点注意,如果覆写了 equals 方法的话,也需要将 hashCode 覆写了。这一点也好理解,如果两个对象是相等的,那么这两者的所有内容包括哈希值也应该相同才对。在集合中(如 List),往往会调用 equals 方法,来判断存入的对象是否相同。

在写 equals 时,往往可以参考 Java 中其他类的 equals 方法。这里先给出一个取自于 android.health.connect.datatypes.units.Lengthequals 方法,大家在写的时候可以参照:

@Override
public boolean equals(Object object) {
    if (this == object) return true;
    if (object instanceof Length) {
        Length other = (Length) object;
        return this.getInMeters() == other.getInMeters();
    }
    return false;
}

4. clone() 方法

protected native Object clone() throws CloneNotSupportedException;

首先要注意到,这个方法是 protected 的。对于 Object 来说,这个方法返回当前对象的一个浅拷贝,而且只有实现了 Cloneable 接口才可以调用该方法,否则抛出 CloneNotSupportedException 异常。

另外提一点,通过调用 clone 方法创建的对象,是不会调用其构造方法的。 其实这个方法是比较鸡肋的方法,Cloneable 这个注解也并不是一个好的设计。应该避免使用。

5. toString() 方法

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

这个方法太常用了,一般子类都会覆写。用于返回对象信息。

线程相关方法

1. wait() 方法

public final void wait() throws InterruptedException {
        wait(0L);
    }

其实线程相关的这几个方法都是关联的,懂了其中两个方法就懂了其他的。关键还是在于对锁的理解。

大家基本都知道这个方法就是让线程等待,但怎么等待,又怎么唤醒,估计大部分人很难说明其用法。

首先,这个 wait 方法确实是让线程等待,但其与 sleep 不同,如果你直接在线程中的调用,会出现java.lang.IllegalMonitorStateException 异常,如下:

public class Hello {
    public static void main(String[] args) {
        Object obj = new Object();
        System.out.println("before wait---");
        try {
            obj.wait();
        } catch (InterruptedException exception) {
            exception.printStackTrace();
        }
        System.out.println("after wait---");
    }
}

异常信息:

mi@mi-HP:~/develop/code/JavaCode$ java Hello.java 
before wait---
Exception in thread "main" java.lang.IllegalMonitorStateException: current thread is not owner
        at java.base/java.lang.Object.wait0(Native Method)
        at java.base/java.lang.Object.wait(Object.java:375)
        at java.base/java.lang.Object.wait(Object.java:348)
        at Hello.main(Hello.java:24)

可以看到,走到 obj.wait() 时发生了崩溃,IllegalMonitorStateException 是一个运行时异常,翻译过来就是“非法监视器状态异常”。它表示线程在没有持有相应监视器锁的情况下执行 waitnotify 等操作,而后面的描述 “current thread is not owner”,也表示当前线程并不是持有者。那么当前线程不是谁的持有者呢?

Java 规定,只有已经获取锁的线程,才可以调用锁的 wait()notify()方法,这个锁是同步代码块,也可以是同步方法。上面说的线程不是持有者,其实就是这个锁的持有者。下面我们更改一下代码:

    public static void main(String[] args) {
        Object obj = new Object();
        synchronized(obj) {            //同步代码块,持有锁 obj
            System.out.println("before wait---");
            try {
                obj.wait();
            } catch (InterruptedException exception) {
                exception.printStackTrace();
            }
            System.out.println("after wait---");
        }
    }

再运行一下,程序正常运行,没有抛出 IllegalMonitorStateException 异常,并在打印 “before wait---” 后等待在那里,线程进入阻塞状态:

mi@mi-HP:~/develop/code/JavaCode$ java Hello.java 
before wait---

此处注意,你在 synchronized 中添加的锁对象,必须与你调用 wait 方法的对象一致,否则仍然会出现 IllegalMonitorStateException 异常。简单来说就是你在哪个对象上调用 wait,就应该将这个对象作为锁持有。

那么有人就问了,为啥这么设计,这么设计有什么用,适用于什么场景?

之所以 wait 方法需要在同步方法或是同步代码块中调用(synchronized),是因为 wait 就是释放当前的锁,既然要释放,那么就意味着必须得先得到这个锁。而调用 notifynotifyAll 是将锁交给含有 wait 方法的线程,让其继续执行下去。如果自身没有锁,那么唤醒其他 wait 的线程让其参与锁的竞争就无从谈起了。

在 Java 平台中,每个对象都有一个唯一与之对应的内部锁(Monitor),此外,Java 虚拟机会为每个对象维护两个集合:一个 EntrySet(入口集),一个 WaitSet(等待集)。对于任意对象 obj,其 EntrySet 用于存储等待获取 obj 对应的内部锁的所有线程,WaitSet 用于存储执行了 obj.waitobj.wait(long) 的线程。

对于对象的非同步方法,任意时刻,可以有任意个线程调用该方法。

对于对象的同步方法,只有拥有这个对象的锁,才能调用这个同步方法。如果这个锁被其他线程占用,那么另外一个调用该同步方法的线程就会处于阻塞状态,并进入这个对象的 EntrySet。 若一个已经拥有独占锁的线程调用了该对象 wait 方法,那么该线程会释放独占锁,并加入到 WaitSet

那么为什么线程都持有了这个锁了,明明可以执行相关任务,为什么会调用 wait 释放锁呢,这个线程的后续任务怎么执行呢?对于这种问题,我们可以这样想象这种场景,你占用了一个房间,准备把老师布置的作业写完,可你正写到一半,另一位同学突然进来,说要占用这个房间开个会。此时你就需要释放这个房间,然后等待这个同学开完会再把房间给你,你继续使用。

你占用房间那就是 synchronized(房间),你暂时释放房间就是 房间.wait,此时你进入到 房间的 WaitSet;别人用完了房间通知你,就是 房间.notify,然后你进入到 房间的EntrySet,等竞争到 房间 后继续写作业。

而某个线程调用 notify()notifyAll() 方法,就是将 WaitSet 中的线程转移到 EntrySet,然后让他们竞争锁。

由此可见,无论你是 wait 还是 notify,都是对这个对象的锁的操作,因此你必须先持有这个对象锁,否则就是 IllegalMonitorStateException 异常。

下面来看一个简单的代码:

public static void main(String[] args) {
        Object obj = new Object();
        Thread thread_1 = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized(obj) {
                    try{
                        System.out.println("threa_1 before wait... "+Thread.currentThread().getState());
                        obj.wait();
                        System.out.println("threa_1 after wait... "+Thread.currentThread().getState());
                    } catch (IllegalMonitorStateException|InterruptedException exception) {
                        exception.printStackTrace();
                    }
                }
            }
        }, "thread_1");

        Thread thread_2 = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized(obj) {
                    try{
                        System.out.println("thread_2 sleep 2 seconds...");
                        Thread.sleep(2000);
                        System.out.println("thread_2 notify...");
                        obj.notify();
                    } catch (IllegalMonitorStateException|InterruptedException exception) {
                        exception.printStackTrace();
                    }
                }
            }
        }, "thread_2");

        thread_1.start();
        thread_2.start();
    }

thread_1 执行时调用 wait 方法,两秒后被 thread_2 调用 notify 唤醒,唤醒后 thread_1 继续执行。 输出结果:

mi@mi-HP:~/develop/code/JavaCode$ java Hello.java 
threa_1 before wait... RUNNABLE
thread_2 sleep 2 seconds...
thread_2 notify...
threa_1 after wait... RUNNABLE

这里还有需要注意的一点就是,虽然 thread_1 被唤醒,但是 thread_1 线程并不是能立即执行的。被唤醒只是说明 thread_1objWaitSet 进入到了 EntrySet,此时的线程状态是 BLOCKED,还需要竞争 obj 锁。当得到 obj 锁之后,才能够继续执行。 诸位可以在 thread_2notify 之后加上 sleep 两秒看看效果。

好了,为了讲解 wait 方法,这里扯了一大堆关于线程等待与唤醒的内容,也只有理解了这些内容,才能明白 wait 方法的作用。

那么这里总结一下,wait 方法用于同步代码块中,用于让当前线程等待,进入对象的 WaitSet。其他线程需要调用对象的 notify 方法,使其被唤醒,进入 EntrySet,再竞争对象锁,获取锁之后将继续执行。

2. wait(long) 方法

public final void wait(long timeoutMillis) throws InterruptedException {
    long comp = Blocker.begin();
    try {
        wait0(timeoutMillis);
    } catch (InterruptedException e) {
        Thread thread = Thread.currentThread();
        if (thread.isVirtual())
            thread.getAndClearInterrupt();
        throw e;
    } finally {
        Blocker.end(comp);
    }
}

wait 方法是无限期等待,必须其他线程调用 notify,而这个带参数的,就是限定了等待时间,超过了这个时间,线程会自己唤醒自己。

另外,通过 wait 的代码可以看到,当参数为 0 时,这个方法其实就是 wait 的无限期等待。而这个方法中,真正让线程进入等待的是 wait0 这个 native 方法:

private final native void wait0(long timeoutMillis) throws InterruptedException;

3. wait(long, int) 方法

public final void wait(long timeoutMillis, int nanos) throws InterruptedException {
    if (timeoutMillis < 0) {
        throw new IllegalArgumentException("timeoutMillis value is negative");
    }

    if (nanos < 0 || nanos > 999999) {
        throw new IllegalArgumentException(
                            "nanosecond timeout value out of range");
    }

    if (nanos > 0 && timeoutMillis < Long.MAX_VALUE) {
        timeoutMillis++;
    }

    wait(timeoutMillis);
}

该方法与 wait(long timeout) 方法类似,只是多了一个 nanos 参数,这个参数表示额外时间(以纳秒为单位,范围是 0-999999)。 所以超时的时间还需要加上 nanos 纳秒。

如果 timeoutnanos 参数都为 0,则不会超时,会一直进行等待,等同于 wait() 方法。

4. notify 方法

public final native void notify();

这个方法前面说过,是用于唤醒 WaitSet 中的线程,使其进入到 EntrySet 中。但是往后会发现还有一个 notifyAll 的方法,那么这两个方法有什么区别呢?

当你调用 notify 时,只有一个等待线程会被唤醒而且它不能保证哪个线程会被唤醒,这取决于线程调度器。虽然如果你调用 notifyAll 方法,那么等待该锁的所有线程都会被唤醒,但是在执行剩余的代码之前,所有被唤醒的线程都将争夺锁定。简单来说,notify 只会唤醒一个线程,notifyAll 将唤醒所有线程。

5. notifyAll 方法

public final native void notifyAll();

唤醒所有等待中的线程。

在线程中,生产者和消费者模型是我们常常用以演示线程同步的,下面是一个典型的生产者消费者例子,看懂了这个例子,waitnotify 基本就没什么问题了:

public class Main {
    private static final Queue<Integer> queue = new LinkedList<>();
    private static final int MAX_SIZE = 5;
    private static final Object lock = new Object();

    public static void main(String[] args) {
        Thread producer = new Thread(() -> {
            while (true) {
                synchronized (lock) {
                    while (queue.size() == MAX_SIZE) {
                        try {
                            lock.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    queue.add(1);
                    lock.notifyAll();
                }
            }
        });

        Thread consumer = new Thread(() -> {
            while (true) {
                synchronized (lock) {
                    while (queue.isEmpty()) {
                        try {
                            lock.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    queue.poll();
                    lock.notifyAll();
                }
            }
        });

        producer.start();
        consumer.start();
    }
}

finalize() 方法

protected void finalize() throws Throwable { }

最后,我们来说一下 finalize 方法,这个方法虽然被标记废弃,但是之前还是比较常用的。它在对象被 GC 回收之前调用,一般覆写这个方法完成这个对象的清理工作,例如清理相关的 native 资源或是其他资源(socket、文件)的释放。

当对象变成(GC Roots)不可达时,GC会判断该对象是否覆盖了 finalize 方法,若未覆盖,则直接将其回收。否则,若对象未执行过 finalize 方法,将其移动到一个队列里,由一个低优先级线程执行该队列中对象的 finalize 方法。执行 finalize 方法完毕后,这些对象才成为真正的垃圾,等待下一轮垃圾回收。

以下是一个 finalize 使用例子:

class FinalizeObj {

    private long nativePointer;

    public FinalizeObj() {
        nativePointer = createNative();
    }

    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        releaseNative(nativePointer);
        nativePointer = 0L;
    }

    private native long createNative();
    private native void releaseNative(long nativePointer);
}

这个例子,在构造方法中创建 native 底层资源,在 finalize 方法中释放 native 底层资源。

不过,现在由于 finalize 被标记为废弃,已经不推荐这么写了。至于为什么会被标记为废弃,主要是因为其被执行的不确定性太大,一个对象从不可达到 finalize 方法被执行,完全依赖 JVM。这无法保证此对象被占用的资源被及时回收,甚至都不能保证这个方法被执行。因此要避免使用。 其实如果这个方法真的好用的话,也不会有那么多的类要提供 closedestroy 等方法了。

那么既然这个方法不推荐,那我要释放上面那个例子中的 native 资源,应该怎么做呢?答案是使用 java.lang.ref.Cleaner,这是 Java 9 推出的一个轻量级垃圾回收机制。不过这个类加到文章里来就太长了。

总结

通过这篇文章,大家应该对 Object 里面的那些方法有一些了解,常用的5个方法较为简单。主要是与线程相关的方法,这才是 Object 类的重头戏。好在只要掌握的 waitnotify 方法,其他的就明白了。最后文章讲解了一下 finalize 方法,作为一个被废弃的方法,我们了解了它的使用方法,后续需要用 Cleaner 等方法替代。