Java的Object 类的完整路径是java.lang.Object ,是所有类的父类编译,当我们创建一个类时,如果没有明确继承一个父类,那么它就会自动继承 Object,成为 Object 的子类(隐式继承)。Object类有九大常用方法,分别是getClass()、finalize()、toString()、equals()、hashcode()、wait()、notify()、notifyAll()和clone()。
一、getClass()
首先,getClass()方法用于获取一个对象的运行时类(Class),进而通过返回的Class对象获取Person的相关信息,比如获取该类的构造方法、该类有哪些方法、该类有哪些成员变量等信息。不同VM针对Class做了不同的优化,所以getClass()的实现也并不相同:
这是因为是Java默认的Hotspot虚拟机并没有开辟单独的Method Area空间,而是有GC Heap的老生代的Metaspace实现的。而Android采用ART VM,这才造成了这种差异。想深入了解不同VM的实现的运行时数据分区、ClassLoader和Class类要读很多书,本文不做过多讨论。
二、finalize()
finalize()是Object的protected方法,在发生GC时触发该方法,大致流程是当对象变成GC Roots不可达时,GC判断该对象是否覆盖了finalize()方法,若未覆盖,则直接将其回收。否则,若对象未执行过finalize()方法,将其放入F-Queue队列,由一低优先级线程执行该队列中对象的finalize()方法。执行finalize()方法完毕后,GC会再次判断该对象是否可达,若不可达,则进行回收;否则,对象“复活”。
子类可以override方法以实现(1)防止对象被回收、(2)防止对象不被回收。防止对象被回收只需让该对象与GC ROOTS之间存在可达链即可。我们重点看看FileInputStream、FileOutputStream、Connection等类怎么防止用户忘记释放资源呢,如下是FileInputStream的部分源码:
三、toString()
toString()方法返回该对象的String表示,这也是连每个初级程序员都很熟悉的一个方法了。你仔细读过Java的源码,就知道很多类的toString()方法都是精雕细琢的,就像Integer的toString()方法,就针对Android做了一定适配:
在实际开发中,复杂对象的toString()方法用Gson生成JSON来实现。
四、equals()和hashcode()
equals()方法和hashcode()方法,我要放在一起说。
一般来说==比较的是引用是否相同,而equals()则是需要重写来比较值是否相同。重写equals()要注意以下几点注意事项:
(1)对任意x,x.equals(x)一定返回true
(2)对任意x,y,如果x.equals(y)返回true,则y.equals(x)也一定返回true
(3)对任意x,y,z,如果x.equals(y)返回true,y.equals(z)也返回true,则x.equals(z)也一定返回true
(4)对任意x,y,如果对象中用于比较的信息没有改变,那么无论调用多少次x.equals(y),返回的结果应该保持一致,要么一直返回true,要么一直返回false
(5)对任意不是null的x,x.equals(null)一定返回false,如果两个对象equals()方法相等则它们的hashCode返回值一定要相同。我们先看一下String是如何实现equals()的:
我们再看一下String是如何实现hashcode()的:
反之,如果两个对象的hashCode返回值相同,它们的equals()方法可以不返回true。这种情况叫做hash碰撞。HashMap处理hash碰撞的方法叫链地址法,除此以外hash碰撞还可以用ArrayMap采用的开放地址法解决,这些不在今天的话题讨论范围之内,不做赘述。
五、wait()、notify()和notifyAll()
wait()、notify()和notifyAll()三个方法实现了Java的wait-notify机制。
先看wait()方法,wait()方法用来让持有此对象的监视器的线程处于阻塞状态,有参数不同的三个同名方法:
ns是纳秒的意思,1s == 1,000,000,000ns,光速是世界上最快的速度,光在1ns时间内仅能传播0.3m。一般PC的CPU计算一道简单指令,比如2+3=5的时间为2~4ns。我们一般只用一个参数的wait()方法或者无参方法就足够了,第二个参数在现实开发中几乎用不到。
含参的wait()方法调用后,线程可以在等待时间结束后进入就绪状态(以下简称“唤醒”);无参的wait()方法调用后,则必须等待持有该对象监视器的线程主动调用notify()或notifyAll()方法后才能被唤醒。区别在于notify()方法唤醒在此对象监视器上等待的单个线程,如果所有线程都在此对象上等待,则会随机唤醒其中一个线程;而notifyAll()方法则唤醒在此对象监视器上等待的所有线程。
wait()、notify()和notifyAll()都是final native方法,我们暂时不需要深入理解内部是怎样实现的,我们只要知道这就是Java的等待-通知(wait-notify)机制,学习它们的应用场景就好了。
打个比方:
(1)用人单位决定录用程序员的时间是不确定的,比如可能要面试好多人,需要综合考虑,不能及时反馈
(2)每个程序员面试结束后需要wait()
(3)用人单位综合考虑之后觉得最合适的程序员,让HR notify()
(4)最后被选中程序员高高兴兴去上班了,其他程序员就等着吧
在开发中,wait-notify机制的最广泛用途就是实现生产者/消费者模型,生产者/消费者模型能解决绝大多数并发问题,通过平衡生产线程和消费线程的工作能力来提高程序的整体处理数据的速度。
使用wait-notify机制的注意事项:
(1)wait()、notify()和notifyAll()必须在synchronized修饰的方法或代码块中使用
(2)在while循环里而不是if语句下使用wait(),确保在线程睡眠前后都检查wait()触发的条件(防止虚假唤醒)
(3)wait()方法必须在多线程共享的对象上调用
我们先定义注意(1)和(2)的一个生产者,往队列里添加元素:
再定义一个一模一样的消费者,除了从队列里移除元素之外,其他代码同上
最后编写符合(3)的测试代码:
查看运行结果:
Produce 1604006010
Produce 1312202442
Produce -1478853208
Produce 1460408111
Produce 1802825495
Queue is Full
Consume 1604006010
Consume 1312202442
Consume -1478853208
Consume 1460408111
Consume 1802825495
Queue is Empty
除了以上介绍的用synchronized关键字配合Object的wait()/notity()实现,生产者-消费者模型还可以用Lock接口配合Condition的await()、signalAll()实现,此外还可以用BlockingQueue实现,但这些都不在本文的话题讨论范围之内,就不再赘述了。
六、clone()
Java语言的Object类实现了Cloneable接口,一个对象可以通过调用Clone()方法生成对象。需要注意的是,clone()方法并不是Cloneable接口里的,而是Object类里的,Cloneable是一个标识接口,标识这个类的对象是可被拷贝的,如果没有实现Cloneable接口却调用了clone()方法就会报错。
对象除了new出来和clone()出来,还可以通过反射和反序列化两种方式产生,但这两种方式不在我们今天的话题讨论范围之内。
所谓原型模式,就是利用clone()生成对象的设计模式。需要提前了解一下深拷贝和浅拷贝的概念。Java中的数据类型分为基本类型和引用类型,在一个方法里的变量如果是基本类型的话,变量就直接存储在这个方法的栈帧里,例如int、long等;而引用类型则在栈帧里存储这个变量的指针,指向堆中该实体的地址,例如String、Array等。深拷贝和浅拷贝是只针对引用数据类型的
比如一个方法有一个基本类型参数和一个引用类型参数,在方法体里对参数重新赋值,会影响传入的引用类型参数,而不会影响基本类型参数,因为基本类型参数是值传递,而引用类型是引用传递。需要注意的是,较真来说Java只有值传递,因为Java的引用传递传的是引用类型对象在堆内存空间的地址,引用传递只是一种习惯性的说法,这个涉及到JVM和操作系统,不做过多讨论。
先定义一个用户类:
编写测试代码:
Log打印结果如下:
调用前x的值:10
调用后x的值:10
调用前user的值:User{name='唐茜靖, age=18}
调用后user的值:User{name='管晨辰, age=16}
传递基本类型的方法(updateValue())流程图:
传递引用类型的方法(updateUser())流程图:
但也有例外,比如String类型和<=127的Long类型虽然也是引用类型,却像基本类型一样不受影响,这是因为它们会先比较常量池维护的值,这涉及VM的内容,今天不做过多讨论。浅拷贝是在按位(bit)拷贝对象,这个对象有着原始对象属性值的一份精确拷贝。我们结合应用场景分析一下,还是刚才的User类,我们增加一个存放地址的内部类Address,我们需要用户信息可以被其他module查询,但是不允许它们被其他module修改,新增代码如下:
我们可以注意到Address还是指向以前的引用,浅拷贝会带来数据安全方面的隐患,这就到了需要深拷贝的时候了。对于有多层对象的,每个对象都需要实现 Cloneable 并重写 clone() 方法,才可以实现了对象的串行层层拷贝。就像这样:
至于彻底深拷贝几乎是不可能实现的,不但可能存在引用关系非常复杂的情况,也可能存在引用链的某一级上引用了一个没有实现Cloneable接口的第三方对象的情况。
最后总结一下,原型模式的用途之一是保护性拷贝,防止外部对只读对象进行修改,刚才我举的例子就是保护性拷贝。另一个重要用途则是解决构建复杂对象的资源消耗问题,提升创建对象的效率,这是因为clone()方法的原理是在内存中拷贝二进制流,比new一个对象的性能好很多,非常适用于需要在循环体内产生大量对象的时候。绝大多数设计模式都是牺牲性能提升开发效率的,原型模式是为数不多的牺牲开发效率提升性能的。我们做一下new和clone的对比:
通过ASM工具查看bytecode,可以对比出二者对栈资源的消耗:
// access flags 0x1
public testNew()V
……省略……
MAXSTACK = 4
MAXLOCALS = 2
// access flags 0x1
public testClone()V
MAXSTACK = 1
此外还需要注意一点,拷贝不会执行构造函数,所以有时候我们需要注意这个潜在的问题。幸好这个问题不是不可避免的,这是Android第五大组件Intent的clone()的实现,没有用拷贝: