Object 概述
Object
类是 Java 中所有类的父类,这个类中包含了若干方法,这也就意味着所有类都将继承这些方法。因此,掌握这个类的方法是非常必要的,毕竟所有类都能为你提供这些方法。
Object
类位于 java.base
模块下 java.lang.Object
,其结构如下:
其中 wait0
是私有方法,不用管。我把这些方法分为两类,一类是常用方法(如 hashCode
、equals
);一类是线程相关方法(如 wait
、notify
)。
下面就对这些方法一一说明,进而全面掌握这个类的所有方法。
常用方法
1. getClass() 方法
public final native Class<?> getClass();
返回这个类对应的 Class
对象。final
方法,子类无法覆写。Class
对象是 Java 反射中最重要的一个类。有关反射的内容可以查看这个文章:Java Reflection 反射使用 完全指南
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.Length
的 equals
方法,大家在写的时候可以参照:
@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
是一个运行时异常,翻译过来就是“非法监视器状态异常”。它表示线程在没有持有相应监视器锁的情况下执行 wait
、notify
等操作,而后面的描述 “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
就是释放当前的锁,既然要释放,那么就意味着必须得先得到这个锁。而调用 notify
、notifyAll
是将锁交给含有 wait
方法的线程,让其继续执行下去。如果自身没有锁,那么唤醒其他 wait
的线程让其参与锁的竞争就无从谈起了。
在 Java 平台中,每个对象都有一个唯一与之对应的内部锁(Monitor
),此外,Java 虚拟机会为每个对象维护两个集合:一个 EntrySet
(入口集),一个 WaitSet
(等待集)。对于任意对象 obj
,其 EntrySet
用于存储等待获取 obj
对应的内部锁的所有线程,WaitSet
用于存储执行了 obj.wait
、obj.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_1
从 obj
的 WaitSet
进入到了 EntrySet
,此时的线程状态是 BLOCKED
,还需要竞争 obj
锁。当得到 obj
锁之后,才能够继续执行。
诸位可以在 thread_2
的 notify
之后加上 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
纳秒。
如果 timeout
与 nanos
参数都为 0,则不会超时,会一直进行等待,等同于 wait()
方法。
4. notify 方法
public final native void notify();
这个方法前面说过,是用于唤醒 WaitSet
中的线程,使其进入到 EntrySet
中。但是往后会发现还有一个 notifyAll
的方法,那么这两个方法有什么区别呢?
当你调用 notify
时,只有一个等待线程会被唤醒而且它不能保证哪个线程会被唤醒,这取决于线程调度器。虽然如果你调用 notifyAll
方法,那么等待该锁的所有线程都会被唤醒,但是在执行剩余的代码之前,所有被唤醒的线程都将争夺锁定。简单来说,notify
只会唤醒一个线程,notifyAll
将唤醒所有线程。
5. notifyAll 方法
public final native void notifyAll();
唤醒所有等待中的线程。
在线程中,生产者和消费者模型是我们常常用以演示线程同步的,下面是一个典型的生产者消费者例子,看懂了这个例子,wait
和 notify
基本就没什么问题了:
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。这无法保证此对象被占用的资源被及时回收,甚至都不能保证这个方法被执行。因此要避免使用。
其实如果这个方法真的好用的话,也不会有那么多的类要提供 close
、destroy
等方法了。
那么既然这个方法不推荐,那我要释放上面那个例子中的 native
资源,应该怎么做呢?答案是使用 java.lang.ref.Cleaner
,这是 Java 9 推出的一个轻量级垃圾回收机制。不过这个类加到文章里来就太长了。
总结
通过这篇文章,大家应该对 Object
里面的那些方法有一些了解,常用的5个方法较为简单。主要是与线程相关的方法,这才是 Object
类的重头戏。好在只要掌握的 wait
和 notify
方法,其他的就明白了。最后文章讲解了一下 finalize
方法,作为一个被废弃的方法,我们了解了它的使用方法,后续需要用 Cleaner
等方法替代。