深入理解Object类
Object类
文档:Java 12
test JDK version:1.8.0_171
Object类位于Java的顶层,是Java所有类的父类。任何对象,包括数组,都实现了该类的方法。
public class Object {
protected void finalize() throws Throwable { }
}
一. clone()
简单说,就是创建并返回此对象的副本
对于任何对象x:
x.clone() != x 为true,但不绝对;
x.clone().getClass() == x.getClass()为true,但不绝对;
一般情况下,x.clone().equals(x)返回true,但不是强制性的
按照惯例,返回对象应该应该通过调用super.clone来获得。如果一个类及其所有父类(Object类除外)都遵循惯例,那么x.clone.getClass() == x.getClass()也成立。
按照惯例,该方法返回的副本对象应该与原对象独立,为了实现这种独立,在调用super.clone返回对象前,需要修改对象的一个或多个成员。这意味着复制该对象的任何组成内部“深层结构”的可变成员时,需要将其原来的引用都替换为其相应副本的引用。如果类只包含基本数据类型或者不可变的引用类型,那么直接调用super.clone不需要修改任何成员。
当类没有实现Cloneable接口时,一个CloneNotSupportException将被抛出。注意,所有数组都已经实现了Cloneable接口,T[]类型的clone方法返回的也是T[],T可以是任意的引用或基本数据类型。否则,clone方法创建一个新实例,并根据原始对象的成员初始化克隆对象的所有成员,就像赋值一样,但成员的内容本身并不是克隆的,即clone()实现的是该对象的"浅复制",而不是"深复制"。
Object类并没有实现Cloneable接口,因此在任何一个Object类对象中调用此方法会在运行时会抛出异常。
protected native Object clone() throws CloneNotSupportedException;
- 被修饰符protected修饰,表明子类可以访问此方法。
- 修饰符native表明Java不负责该方法的实现
文档可能翻译地不太好,下面重新叙述一下:
对于任何对象x:(下面不是强制性的)
- 克隆对象具有新的内存地址
- 克隆对象和原对象属于相同的类
- 克隆对象和原对象相等(equals方法返回true)
调用clone函数时,我们往往希望克隆对象和原对象是相互独立的,即修改克隆对象不会对原对象造成任何改变。单纯地调用super.clone实现的只是“浅复制”,在这种情况下,如果原对象具有某些可变的引用类型成员(即所谓的深层结构,引用类型成员和原对象的引用类型成员仍然指向同一个内存地址)时,此时修改克隆对象的引用类型成员可能也会改变原对象,因此我们需要进行“深复制”,修改那些可变的引用类型成员并指向其相应的副本。
同样地,T[]数组类型对象的clone方法也是浅复制,当T是值类型数据时没有问题,对于引用类型时,则只是引用的直接复制。
二. equals()
表明其他对象是否和此对象“相等”,"=="是指在内存是否为同一块存储单元,如果需要比较内容相等需要override,可以参考深入理解hashCode和equals
public boolean equals(Object obj) {
return (this == obj);
}
三. hashCode()
返回对象的hash码,重写equals()往往也需要重写此方法,在一些具有哈希功能的Collection(比如HashMap)中用到,比较重要的一点:凡是"相等"(equals比较返回true)的两个对象,其hash码也相等,可以参考深入理解hashCode和equals
public native int hashCode();
四. finalize()(java 9以后废弃)
当垃圾收集确定不再有对该对象的引用时,垃圾收集器在该对象上调用此方法。对于任何对象,此方法只会调用一次。
将会导致性能问题、死锁还有挂起等。清理过程中的错误可能会导致资源泄漏,当清理过程不必要时也无法取消清理,清理不同的对象时没有保证顺序,也无法保证定时清理。
类实例持有非堆资源时应该提供一个方法来显式地释放这些资源,并在必要的情况下,实现AutoCloseable。当一个对象不可达时,Cleaner(清理器)还有PhantomReference(虚引用)提供了更加灵活且高效的释放资源的方式。
protected void finalize() throws Throwable { }
当垃圾收集确定不再有对该对象的引用时,垃圾收集器在该对象上调用此方法。子类可以覆盖此方法来处理系统资源或执行其他清理。
Object类的finalize方法实现为空,只是简单地返回,Object的子类可能会重写它。
Java并不确定哪个线程将会执行某个对象的finalize方法,但可以确定当finalize被调用时,调用线程将不会持有任何用户可见的同步锁。如果一个finalize中出现未被捕获的异常,则异常将被忽略,该对象的finalizization也将终止。
一个对象的finalize被调用时,只有当Java虚拟机再一次检测不到任何能访问该对象的活跃线程时,清理才会进行。
五. getClass()
返回此对象的运行时类,返回的对象是一个被静态同步方法锁住的对象,这个对象可以代表此对象所属的类
public final native Class<?> getClass();
下面看一个例子
Number n1 = 0;
Number n2 = 1;
Class clazz1 = n1.getClass();
Class clazz2 = n2.getClass();
System.out.println(clazz1.toString());
System.out.println(clazz2.toString());
System.out.println(clazz1.hashCode());
System.out.println(clazz2.hashCode());
System.out.println(clazz1.hashCode() == clazz2.hashCode());
Integer是Number的子类,此时n1和n2的运行时类均为子类Integer,调用getClass返回的对象是一个能够代表(描述)Integer类的对象,通过调用hashCode方法(类“Class”没有重写hashCode,可以理解为直接返回内存地址)可以看到此时clazz1和clazz2指向内存中的同一个对象。
- 被final修饰,表明其不可重写
- native表明Java不负责方法的具体实现
六. notify函数
关于notify和wait,需要了解一下几个概念:
线程的等待状态、阻塞状态
对象锁(对象监视器)、等待集(池)、锁池(自行百度)
public final native void notify();
public final native void notifyAll();
唤醒正在此对象监视器等待的的单个线程,或者说随机选择一个在该对象上调用wait方法的线程,解除其阻塞状态,该方法只能在同步方法或同步块内部调用。
- 被final修饰,表明其不可重写
- native表明Java不负责方法的具体实现
如果多个线程想获得对象锁(均处于阻塞状态),将会从中选择一个进行唤醒。如何选择线程则由实现决定(随机)。而线程通过调用wait方法来等待对象的***。
被唤醒的线程不会马上运行直到当前线程解开对象锁。被唤醒的线程将会和其它在对该对象进行同步的活跃线程公平竞争。举例来说,被唤醒线程在成为下一个能对该对象加锁的线程并没有优势或劣势
只有拥有该对象***的线程才能调用此方法。一个线程拥有对象的***有三种方式:
- 通过该对象的一个同步实例方法;
- 通过执行对对象进行同步的同步语句主体
- 对于类类型的对象,通过执行该类的同步静态方法
每次只有一个线程能够拥有该对象的***。当当前线程不是对象***的拥有者时,调用notify将会抛出一个IllegalMonitorStateException异常
唤醒正在此对象监视器等待的所有线程,或者说解除那些在该对象上调用wait()方法的线程的阻塞状态,该方法只能在同步方法或同步块内部调用,其余与notify()一样
- 被final修饰,表明其不可重写
- native表明Java不负责方法的具体实现
唤醒等待该对象***的所有线程,线程通过调用wait方法来等待对象的***。
被唤醒的线程不会马上运行直到当前线程解开对象的锁,被唤醒的线程将会和其它在对该对象进行同步的活跃线程公平竞争。举例来说,被唤醒线程在成为下一个能对该对象加锁的线程并没有绝对的优势或劣势。
只有拥有该对象***的线程才能调用此方法。同notify中的一样,一个线程拥有对象的***有三种方式。
每次只有一个线程能够拥有该对象的***。当当前线程不是对象***的拥有者时,调用notify也会抛出一个IllegalMonitorStateException异常
七. toString()
返回此对象的一个字符串表示,对象所属类类名 + “@” + 对象哈希码
public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
八. wait函数
使当前线程等待直至被唤醒,通常是通过通知(notify)或者中断,详见wait(long timeout, int nanos)
(该方法只能在一个同步方法中调用)
public final void wait() throws InterruptedException {
wait(0);
}
使当前线程等待直至被唤醒,通常是通过通知(notify)或者中断,或者超时(timeout * ms + nanos * ns),从源码中可以看到,nanos>0时,也就增加了1ms。
(该方法只能在一个同步方法中调用)
public final void wait(long timeout, int nanos) throws InterruptedException {
if (timeout < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
if (nanos < 0 || nanos > 999999) {
throw new IllegalArgumentException(
"nanosecond timeout value out of range");
}
if (nanos > 0) {
timeout++;
}
wait(timeout);
}
当前线程必须拥有该对象的***锁,一个对象获得锁的三种方式可以查看notify函数。
该方法将会使得当前线程T放弃任何对该对象上的同步声明,并进入该对象的等待池中(注意到,只有该对象的锁被释放,在线程T等待期间,线程T施加在其它对象上的同步仍然是被锁的)
然后线程T无法被调度,并处于休眠状态中,直到以下情况之一发生:
1.其它线程调用了该对象的notify函数,而线程T被选中为下一个被唤醒的线程;
2.其它线程调用了该对象的notifyAll函数;
3.其它线程中断了线程T;
4.计时等待,指定的实时时间结束。但如果timeoutMills和和nanos均为0时,则不考虑超时,线程只能通过上述条件之一被唤醒;
5.线程被虚假地唤醒。
然后线程T将会从该对象的等待集(池)中移除并能被线程调度。T将和其他线程公平竞争该对象的同步权。T一旦重新获得了该对象的控制权,那么T对对象的同步将恢复到调用wait之前。然后线程T从wait调用中返回。因此,在wait调用返回后,对象和线程T的同步状态与调用wait方法时相同。
一个线程可以被虚假地唤醒(不需要被通知,终端或者超时)。然后在实际中很少发生,应用程序必须测试能够唤醒线程的条件,来防止这种情况发生,当条件不满足时,线程应该继续等待。
如果当前线程在等待之前或者等待期间被中断,则抛出一个InterruptedException异常,当异常抛出时,当前线程的终端状态将清除。只有当对象锁被恢复时,这个异常才回抛出。
简单理解,当线程T具有该对象的对象锁时(一定是在某个同步语句块中),此时调用该对象的wait方法,线程T将进入等待池中,并释放该对象的对象锁,当其它的某个线程调用notify时(需要等该线程执行完整个同步语句才释放对象锁),线程T可能(随机)会重新获得对象锁(调用notifyAll时,时,所有线程公平竞争对象锁),此时将恢复至调用wait方法前(即又进入到同步语句块中,但不是重新进入,而是在wait语句后面)
public final native void wait(long timeout) throws InterruptedException;
- 被final修饰,表明其不可重写
- native表明Java不负责方法的具体实现
- 上述wait实现最后都调用了此方法