0%

Java中的Object类

前言

作为java中的万类之源,很有必要了解一下这个类。Object作为万类之基,每个类都以Object作为基类,就连数组也实现了Object的所有方法。

构造器

Object类的构造器,有且仅有一个,就是一个无参构造器。

getClass

首先这个方法被声明成了final,它的作用是返回该Object的运行时的类。返回的类对象是被static synchronized方法修饰的。

真正的返回类型是Class<? extends |x|>,其中的|x|是对调用这个表达式的类型擦除的表达。

我的理解是,这个方法会返回对象的类对象,因为java一切都是对象,所以类也可以由对象来表示,这在java中就是java.lang.Class,而getClass就是用来获取Class对象的一种方法。

hashCode

返回对象的哈希值。这个方法主要是被诸如Hashmap等类使用。hashcode的一般约定为:

  • 在一个java应用程序运行的过程中,对一个相同的对象多次执行必须返回相同的int数字。但是如果切换了程序,那么可以不用保持相同。
  • 如果两个对象用equals判断返回的结果是True,那么它们的hashCode方法返回结果也必须相同。
  • 如果两个对象用equals判断返回的结果是False,那么不一定hashcode也非要不同。但是如果能够生成不同的hashcode,能够有效提升类似HashMap的性能。

在合理可行的范围内,一般都是通过对象的内存地址来生成整数,所以不同的对象之间会生成不同的hashcode,但是java语言本身其实并没有这个要求。

这个方法很简单,equals方法如果是True,也就是认为这两个对象是相同的,那么hashcode必然要相同。而如果equals是false,那么hashcode怎么做都可以。

equals

表示了当前的对象是否和另外一个对象“相同”。如果一个对象是非空的,那么有以下的规则:

  • 自反性。x.equals(x)永远为true
  • 对称性。x.equals(y)和y.equals(x)的真假性必须一致。
  • 传递性。
  • 一致性。如果两个对象没有发生变化,那么多次调用equals方法必须保证相同的结果。
  • 非空对象和null永远返回false。

在Object中的实现中,实现的是最有区别的关系。在默认实现中,当且仅当两个引用引用相同的对象时,才会返回true。

在上面我们对比了非空对象和非空对象、非空对象和空对象,那为什么没有空对象和空对象呢?因为空对象怎么可以调用方法呢,这会引发NPE的。

clone

这个方法是protected修饰的(protected修饰符的作用:如果子类和父类不在一个包中,显然我们的类不可能和Object在一个包中;子类的实例只可以访问子类继承来的protected方法,并不能访问父类的protected方法,这点保证了如果你的类没有重写clone方法,那么这个类的实例化对象是不能调用clone的)。

这个方法的含义是,创建并返回一个副本。而这个“copy”的含义取决于当前的对象。

通常,对于任何的对象x:x.clone() != x必须是true(因为地址不同);x.clone().getClass() == x.getClass()建议为true,但是这并不是绝对要求;x.clone.equals(x)建议为true,同样不是必须的。

从C语言的角度来说,clone的本质,其实就是开辟了一块区域,然后让一个指针指向了之前的对象(在堆区域)。所以在堆中,并没有任何的改变,只是在站区域花费了一个指针的大小空间而已。

根据习俗,返回的对象应该是由调用super.clone得到的。只要一个类和所有它的父类(除了Object类外)都遵守这个习俗,那么x.clone().getClass() == x.getClass()

按照习俗,这个方法返回的对象需要能够独立于原来的对象,为了达成这个目标,在super.clone返回之前,修改一个或者多个字段是很有必要的。通常这意味着需要替代那些可变的对象,即把这些可变的对象也复制一遍。如果一个类仅仅包含原始的8个类型或者是包含一些不可变的对象,那么直接使用super.clone即可。

clone方法会执行特殊的复制操作:

  1. 如果这个对象的类并没有实现Cloneable接口,那么直接抛出CloneNotSupportedException。注意!所有的数组都实现了Cloneable接口,并且如果对数组对象(如T [])执行clone方法,返回的就是T[]。
  2. 如果实现了对应的接口,那么就使用原始的类的字段来初始化。也就是常说的,clone是浅拷贝。

注意!Object本身并没有实现Cloneable接口,所以并不能直接使用clone方法。

这个方法平时用的其实比较少,但是还是很重要的。

当我们调用默认的java的clone的方法的时候,jvm会这么做:

  1. 如果这个类只有八大原始类型,那么会创建一个全新的复制,返回这个全新的复制的引用。String虽然不是原始类型,但是由于其特殊性,所以可以认为在这一类中。
  2. 如果这个类除了原始类型,还有别的引用类型,那么只复制这些引用类型的引用,所以在原来的对象和复制好的对象之中,引用是同一个。

当然你完全可以重写clone方法,来自定义这些逻辑,只要遵循以下这些原则,那么你的clone就是深拷贝:

  • 完全无视原始类型和String。
  • 除了原始类型和String,其余所有的引用类型,必须实现了clone(当然正确性由引用类型的类来保证)。
  • 如果有引用类型没有重写clone,那么就使用new来创建一个新的对象。

Ref

toString

返回一个对象的字符串。通常来说,toString方法应该返回当前对象的文字信息。相关的结果对人类来说应该简单易读。推荐所有的类都重写这个方法。

原始的实现是返回一个类名和十六进制的hashCode,中间用@隔离:

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

notify

用了final进行修饰。

唤醒一个正在等待一个对象监视器锁的线程。如果有多个线程在等待对象的监视器锁,那么任选一个。可以通过调用任意一个wait的重载方法来让一个线程等待一个对象的监视器锁。

在当前的线程放弃这个对象的监视器锁之前,就算是运行着的线程也无法执行。大家都是公平竞争。

notify方法应该只能由获得了当前对象的监视器的线程来调用。一个线程可以通过以下三个方法成为当前对象的监视器:

  • 执行类实例的synchronized方法。
  • 执行对象synchronized{}里面的代码
  • 静态synchronized方法。

注意,在同一时间,仅仅有一个线程可以拥有对象的监视器。

如果当前的线程并没有对象的监视器,那么就会抛出IllegalMonitorStateException

notifyAll

唤醒所有正在等待当前对象监视器的线程。线程可以通过调用对象的wait的方法来等待对象的监视器。

其他部分和notify是一致的。

wait

让当前的线程去等待,直到其他的线程调用了同一个对象的notify或者notifyAll方法,或者经过了一段特定的时间之后。

当前的线程必须拥有监视器。

这个方法会导致当前的线程(后面简称T)将自己置于对象的等待集(wait set)中,然后放弃所有对这个对象的同步锁。然后当前的线程就不会参与到线程调度中去了(也就是再也不会被调度到了),直到下面的四件事情之一发生:

  • 其他的线程调用了该对象的notify方法,然后T正好被选择到了。
  • 其他的线程调用了该对象的notifyAll方法。
  • 其他的线程中断了T
  • 如果设置了时间,那么超过一定时间就会苏醒

然后线程T就会被从等待集(wait set)中移除,然后就会被调度了。然后T就可以正常和其他的线程进行竞争来同步对象了;一旦它获得了对象的控制权,它对对象的所有同步声明都将恢复为原样,也就是恢复到调用wait时候的状态。接下来T从wait方法中返回,因此,从wait方法返回的时候,object和T的状态和调用wait的时候是一致的。

一个线程也可以被一种叫做虚假唤醒的东西唤醒,也就是不通过notify、interrupt或者超时。实际中这种情况很少发生,所以应用程序必须通过测试来防止这种情况出现,在条件不满足的时候应该让线程继续等待,就像下面的代码一样:

1
2
3
4
5
synchronized (obj) {
while (<condition does not hold>)
obj.wait(timeout);
... // Perform action appropriate to condition
}

更加详细的信息可以参考Doug Lea的书籍。

如果当前的线程是在waiting,然而调用了interrupt,就会抛出InterruptedException。这个异常只有当该对象恢复了才会触发。

值得注意的是,wait方法发生在当前的线程,把当前的线程放入到等待集中,对对象解锁,但是其它的对象的锁仍然是锁着的。

这个方法只有当当前线程获取到了对象的监视器之后才可以执行。

finalize

protected修饰的。

当垃圾回收器决定当前的对象没有任何的引用了的时候,就会由垃圾回收器进行调用。子类重写这个方法用来进行资源释放等一些清理动作。

对于这个方法的一般约定是,当java虚拟机确定没有任何线程可以访问对象的时候,就调用finalize。在finalize方法中可以执行任何动作,包括让这个对象重新和其他的线程发生关系。但是finalize的通常目的是执行清理操作。

finalize方法并没有特殊的动作,它只是简单的返回,子类可以重写定义。

java语言并不保证哪个线程会执行对象的finalize方法。但是它保证在调用finalize的时候线程将不会持有任何的synchronize锁。如果finalize抛出了一个没有被捕获的异常,那么异常会被忽略。

finalize方法调用了之后,对象并不是马上被丢弃,而是只有当java虚拟机确定不会再有任何线程通过任何方法来访问这个对象时,才会彻底抛弃这个对象。

finalize方法永远只会被调用一次。

由finalize方法引发的任何异常都将导致此对象的终止终止,但否则将被忽略。

面试常见问题

  • 在重写了equals方法之后需要重写hashcode吗? 需要。因为根据要求,如果两个对象通过equals判断的结果是true,那么hashcode必然要求一样。

  • Object类有多少方法? 千万别直接报个数字。建议慢慢和面试官聊一聊这些方法,简单聊一聊就行。如果面试官非要一个数字,那这种公司赶紧跑吧。