Java Object解析

Java里所有类都继承自Object类,可以说Object类是Java世界的基石,奠定了Java运行的基调。Object类有一些方法,约定了所有Java类需要遵循的规范。今天我们就来分析一下Java Object(以下都有分析均基于JDK7)。

首先Object类有以下这些方法:

  • Object()
  • getClass()
  • hashCode()
  • equals(Object obj)
  • clone()
  • toString()
  • notify()
  • notifyAll()
  • wait(long timeout)
  • wait(long timeout, int nanos)
  • wait()
  • finalize()

其中除了clone方法和finalize方法是protected,其他方法都是public的。这个修饰的关键字是有一些讲究的,在下面说明。

Object()

Object类只有一个默认构造方法,就是Object()。Java所有类在构造实例时都会先调用Object的默认构造方法。

getClass()

getClass方法算是Object类比较重要的方法,返回结果是对象运行时的类型。Java反射机制就是基于Class类,根据反射机制,我们可以获取类的实例,执行类的方法或者获取类的属性值。可以说有了Class,就可以控制这个类的一切。

hashCode()

返回对象的hash值。这个值的意义很重大,代表着对象在内存中的地址,也是对象的唯一标示。
hashCode()有以下规范:

  • 在Java应用运行中,如果对象的equals方法用到的字段值没有发生变化,则多次调用hashCode方法应该返回同样的整数值。但是同一个应用在两次运行之中,同一个对象的hashCode方法返回值不需要保持一致;
  • 如果两个对象调用equals方法比较为true,则两个对象的hashCode方法返回值也应该相同;
  • 没有强制要求,如果两个对象调用equals方法比较为false,则两个对象的hashCode方法返回值必须不同。但是请注意,不同对象生成不同的hash值可能会提高hash表的性能;

equals(Object obj)

表明两个对象是否相等。
equals方法在非null对象上实现了相等关系:

  • 自反性(reflexive):对于任意非null的对象x,x.equals(x)应该返回true;
  • 对称性(symmetric):对于任意非null的对象x/y,如果y.equals(x)返回true,则x.equals(y)应该返回true;
  • 传递性(transitive):对于任意非null的对象x、y和z,如果x.equals(y)返回true且y.equals(z)返回true,则x.equals(z)应该返回true;
  • 一致性(consistent):对于任意非null的对象x、y,如果在equals方法被使用的字段属性没有变化,多次调用x.equals(y)返回的应该是同样的结果;
  • 对于任意非null对象x,x.equals(null)总是返回false;

Object类的equals方法实现了对象中大部分可能出现的可识别的相等关系,即对于任意非null的x、y,如果x==y=true,则x.equals(y)应该返回true。

PS:为了维护hashCode方法的规范-相等的对象必须有相同的hash值,重写equals方法时必须要同时重写hashCode方法。

这边先埋个伏笔,为什么Java规范中总是说重写equals方法同时需要重写hashCode呢,我们在文末解开这个谜题。

clone()

创建并返回对象的副本,副本的精确含义与对象的类型有关。

一般来说,clone方法通过调用super.clone方法获取返回的副本,如果当前调用类及其父类(除了Object)都遵循这个规范,则x.clone().getClass() == x.getClass() = true。

一般来说,clone方法返回的副本对象应该和当前对象保持独立。为达成这个目标,需要修改super.clone方法返回的副本对象的字段。这意味着如果拷贝一个可变对象且对象包含复杂结构的子对象,则需要用子对象拷贝的副本引用替换副本对象中的引用。如果一个类只包含基本类型和不可变对象的引用,则不需要替换super.clone返回副本对象的字段。

Object类的clone方法执行特定的克隆操作。首先,如果这个类没有实现Cloneable接口,clone方法会抛出CloneNotSupportedException异常。要说明的是数组被认为是实现Cloneable接口且一个类型是T[]的数组clone方法返回的对象也是T[]类型的。对象的clone方法返回的是新创建的副本对象,副本对象的字段都是通过现有对象字段组装而来不是这些字段的克隆副本,所以clone方法本质是浅拷贝而不是深拷贝操作。

Object类并未实现Cloneable接口,所以在运行时调用Object对象的clone方法会抛出异常。

toString()

返回对象的字符串表示。总的来说,toString方法返回一个通过文本化表达对象的字符串。返回结果应该是简明但又足够明确、方便阅读的表达方式,Java官方推荐所有的子类都重写toString方法。

Object类的toString方法返回值由对象的类名、@字符和十六进制对象的hash值构成,即逻辑是:

1
getClass().getName() + '@' + Integer.toHexString(hashCode())

notify()

唤醒一个正在等待当前对象监视器的线程。如果有多个线程等待当前对象,那么会随机唤醒其中一个线程。当线程被调用wait方法,则线程会等待当前对象的监视器。

唤醒不代表可以立即执行,被唤醒的线程需要等待当前线程解除对象锁才能继续处理。被唤醒的线程和其他线程一样公平的竞争对象锁,不会因为被唤醒有特殊对待。

只有已经获取对象监视器的线程才能调用notify方法,线程可以通过三种方式获取对象监视器:

  • 执行对象的同步实例方法;
  • 执行对象的同步代码块;
  • 对于Class类型的对象,执行类的同步静态方法;

notifyAll()

唤醒所有等待对象监视器的线程。所有被唤醒的线程等待当前线程解除对象锁之后继续处理,所有被唤醒的线程和其他线程一样公平的竞争对象锁。

和notify方法一样,只有已经获取对象监视器的线程可以调用notifyAll方法。

wait(long timeout)

将导致当前线程等待,直到其他线程调用对象的notify或notifyAll方法,或者等待到指定的时间。

当前线程必须占有对象监视器。

wait方法会将当前线程(T)放到等待对象监视器的集合中,并且解除当前线程对对象的所有同步请求。线程T将一直保持休眠,不接受线程调度管理。除非以下条件出现:

  • 其他线程调用notify方法且线程T被随机选中唤醒;
  • 其他线程调用notifyAll方法;
  • 其他线程中断了线程T;
  • 线程等待时间超过指定时间后;

线程T退出等待状态,参与线程调度,与其他线程平等竞争对象锁的所有权。

wait(long timeout, int nanos)

此方法类似于wait(long timeout)方法,但是允许更精准的超时时间控制。超时时间的纳秒表达式为1000000*timeout+nanos

wait()

相当于wait(0)。

finalize()

当Java的垃圾回收器确认不存在此对象的引用时,垃圾回收器会调用对象的finalize方法。Object类的finalize方法没有任何逻辑,子类可以重写finalize方法以处理系统资源或执行其他的清理。

finalize的一般规约是:当JVM确定任何未终结的线程不存在任何访问到此对象的方式时,JVM会调用对象的finalize方法。finalize方法本身不限制执行任何逻辑,甚至可以让其他线程访问到此对象,避免对象被垃圾回收。但finalize方法的目的一般是在对象会回收之前做一些系统资源清理的动作。

JVM可以保证最多只会调用一次对象的finalize方法,所以如果finalize方法执行中抛出了异常,那么finalize方法将会终止。此时,对象的状态不可知,JVM也不会继续回收这个对象,如果这时对象被调用,可能会无法预知的异常。

彩蛋:为什么所有的Java规范都说重写equals的话,必须重写hashCode?

原因是对于Object类来说,equals方法和hashCode方法表达的是对象在不同层面的信息,equals方法比较的是对象的属性特征,比如某个参数,两个对象是否相同;hashCode方法表达的是对象在内存中存储的地址。目前,Java的官方库中既有根据equals方法判断对象是否相等,也有根据hashCode方法计算的值是否相同来比较对象。比如说HashMap,HashMap在put时,首先根据key的hashCode计算key在数组里的索引,然后判断数组对应位置上是否已经有值,没有值则创建链表直接设置;有值则遍历链表元素,调用equals方法判断对象是否相等,不相等的话则将value加到链表末尾。