前言
作为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方法会执行特殊的复制操作:
- 如果这个对象的类并没有实现Cloneable接口,那么直接抛出
CloneNotSupportedException。注意!所有的数组都实现了Cloneable接口,并且如果对数组对象(如T [])执行clone方法,返回的就是T[]。 - 如果实现了对应的接口,那么就使用原始的类的字段来初始化。也就是常说的,clone是浅拷贝。
注意!Object本身并没有实现Cloneable接口,所以并不能直接使用clone方法。
这个方法平时用的其实比较少,但是还是很重要的。
当我们调用默认的java的clone的方法的时候,jvm会这么做:
- 如果这个类只有八大原始类型,那么会创建一个全新的复制,返回这个全新的复制的引用。String虽然不是原始类型,但是由于其特殊性,所以可以认为在这一类中。
- 如果这个类除了原始类型,还有别的引用类型,那么只复制这些引用类型的引用,所以在原来的对象和复制好的对象之中,引用是同一个。
当然你完全可以重写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 | synchronized (obj) { |
更加详细的信息可以参考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类有多少方法? 千万别直接报个数字。建议慢慢和面试官聊一聊这些方法,简单聊一聊就行。如果面试官非要一个数字,那这种公司赶紧跑吧。