一、概述
理论上Object
类是所有类的父类,即直接或间接的继承java.lang.Object
类。由于所有的类都继承在Object
类,因此省略了extends Object
关键字。
Object
类属于java.lang
包,此包下的所有类在使用时无需手动导入,系统会在程序编译期间自动导入。Object
类是所有类的基类,当一个类没有直接继承某个类时,默认继承Object
类,也就是说任何类都直接或间接继承此类,Object
类中能访问的方法在所有类中都可以调用,下面我们会分别介绍Object
类中的所有方法。
二、类
/**
* Class {@code Object} is the root of the class hierarchy.
* Every class has {@code Object} as a superclass. All objects,
* including arrays, implement the methods of this class.
*
* @author unascribed
* @see java.lang.Class
* @since JDK1.0
*/
public class Object {
private static native void registerNatives();
static {
registerNatives();
}
public final native Class<?> getClass();
public native int hashCode();
public boolean equals(Object obj) {
return (this == obj);
}
protected native Object clone() throws CloneNotSupportedException;
public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
public final native void notify();
public final native void notifyAll();
public final native void wait(long timeout) throws InterruptedException;
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);
}
public final void wait() throws InterruptedException {
wait(0);
}
protected void finalize() throws Throwable { }
}
三、源码解析
3.1 类构造器
我们知道类构造器是创建Java
对象的途径之一,通过new
关键字调用构造器完成对象的实例化,还能通过构造器对对象进行相应的初始化。一个类必须要有一个构造器的存在,如果没有显示声明,那么系统会默认创造一个无参构造器,在JDK
的Object
类源码中,是看不到构造器的,系统会自动添加一个无参构造器。
我们可以通过以下构造一个Object
类的对象。
Object obj = new Object();
3.2 equals方法
通常很多面试题都会问equals()
方法和==
运算符的区别,==
运算符用于比较基本类型的值是否相同,或者比较两个对象的引用是否相等,而equals
用于比较两个对象是否相等,这样说可能比较宽泛,两个对象如何才是相等的呢?这个标尺该如何定?
我们可以看看Object
类中的equals
方法:
public boolean equals(Object obj) {
return (this == obj);
}
可以看到,在Object
类中,==
运算符和equals
方法是等价的,都是比较两个对象的引用是否相等,从另一方面来讲,如果两个对象的引用相等,那么这两个对象一定是相等的。对于我们自定义的一个对象,如果不重写equals
方法,那么在比较对象的时候就是调用Object
类的equals
方法,也就是用==
运算符比较两个对象。我们可以看看String
类中的重写的equals
方法:
public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
if (anObject instanceof String) {
String anotherString = (String)anObject;
int n = value.length;
if (n == anotherString.value.length) {
char v1[] = value;
char v2[] = anotherString.value;
int i = 0;
while (n-- != 0) {
if (v1[i] != v2[i])
return false;
i++;
}
return true;
}
}
return false;
}
String
是引用类型,比较时不能比较引用是否相等,重点是字符串的内容是否相等。所以String
类定义两个对象相等的标准是字符串内容都相同。
在Java
规范中,对equals
方法的使用必须遵循以下几个原则:
- 自反性:对于任何非空引用值x,x.equals(x)都应返回true。
- 对称性:对于任何非空引用值x和y,当且仅当y.equals(x)返回true时,x.equals(y)才应返回true。
- 传递性:对于任何非空引用值x、y和z,如果x.equals(y)返回true,并且y.equals(z)返回true,那么x.equals(z)应返回true。
- 一致性:对于任何非空引用值x和y,多次调用x.equals(y)始终返回true或始终返回false,前提是对象上equals比较中所用的信息没有被修改
- 对于任何非空引用值x,x.equals(null)都应返回false。
下面我们自定义一个Person
类,然后重写其equals
方法,比较两个Person
对象:
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Person {
private String pname;
private int page;
@Override
public boolean equals(Object obj) {
if (this == obj) {//引用相等那么两个对象当然相等
return true;
}
if (obj == null || !(obj instanceof Person)) {//对象为空或者不是Person类的实例
return false;
}
Person otherPerson = (Person)obj;
if (otherPerson.getPname().equals(this.getPname())
&& otherPerson.getPage() == this.getPage()) {
return true;
}
return false;
}
public static void main(String[] args) {
Person p1 = new Person("Tom", 21);
Person p2 = new Person("Marry", 20);
System.out.println(p1 == p2);//false
System.out.println(p1.equals(p2));//false
Person p3 = new Person("Tom", 21);
System.out.println(p1.equals(p3));//true
}
}
通过重写equals
方法,我们自定义两个对象相等的标尺为Person
对象的两个属性都相等,则对象相等,否则不相等。如果不重写equals
方法,那么始终是调用Object
类的equals
方法,也就是用==比较两个对象在栈内存中的引用地址是否相等。
这时候有个Person
类的子类Man
,也重写了equals
方法:
public class Man extends Person {
private String sex;
public Man(String pname, int page, String sex) {
super(pname, page);
this.sex = sex;
}
@Override
public boolean equals(Object obj) {
if (!super.equals(obj)) {
return false;
}
if (obj == null || !(obj instanceof Man)) {//对象为空或者不是Person类的实例
return false;
}
Man man = (Man) obj;
return sex.equals(man.sex);
}
public static void main(String[] args) {
Person p = new Person("Tom", 22);
Man m = new Man("Tom", 22, "男");
System.out.println(p.equals(m));//true
System.out.println(m.equals(p));//false
}
}
通过打印结果我们发现person.equals(man)
得到的结果是true
,而man.equals(person)
得到的结果却是false
,这显然是不正确的。
问题出现在instanceof
关键字上,Man
是Person
的子类,person instanceof Man
结果当然是false
。这违反了我们上面说的对称性。
实际上用instanceof
关键字是做不到对称性的要求的。这里推荐做法是用getClass()
方法取代instanceof
运算符。getClass()
关键字也是Object
类中的一个方法,作用是返回一个对象的运行时类,下面我们会详细讲解。
那么Person
类中的equals
方法为:
public boolean equals(Object obj) {
if (this == obj) {//引用相等那么两个对象当然相等
return true;
}
if (obj == null || (getClass() != obj.getClass())) {//对象为空或者不是Person类的实例
return false;
}
Person otherPerson = (Person)obj;
if (otherPerson.getPname().equals(this.getPname())
&& otherPerson.getPage() == this.getPage()) {
return true;
}
return false;
}
打印结果person.equals(man)
得到的结果是false
,man.equals(person)
得到的结果也是false
,满足对称性。
注意:使用getClass
不是绝对的,要根据情况而定,毕竟定义对象是否相等的标准是由程序员自己定义的。而且使用getClass
不符合多态的定义,比如AbstractSet
抽象类,它有两个子类TreeSet
和HashSet
,他们分别使用不同的算法实现查找集合的操作,但无论集合采用哪种方式实现,都需要拥有对两个集合进行比较的功能,如果使用getClass
实现equals
方法的重写,那么就不能在两个不同子类的对象进行相等的比较。而且集合类比较特殊,其子类是不需要自定义相等的概念的。
所以什么时候使用instanceof
运算符,什么时候使用getClass()
有如下建议:
①、如果子类能够拥有自己的相等概念,则对称性需求将强制采用getClass
进行检测。
②、如果有超类决定相等的概念,那么就可以使用instanceof
进行检测,这样可以在不同的子类的对象之间进行相等的比较。
下面给出一个完美的equals
方法的建议:
- 显示参数命名为
otherObject
,稍后会将它转换成另一个叫做other
的变量。 - 判断比较的两个对象引用是否相等,如果引用相等那么表示是同一个对象,那么当然相等。
- 如果
otherObject
为null
,直接返回false
,表示不相等。 - 比较
this
和otherObject
是否是同一个类:如果equals
的语义在每个子类中有所改变,就使用getClass
检测;如果所有的子类都有统一的定义,那么使用instanceof
检测。 - 将
otherObject
转换成对应的类类型变量。 - 最后对对象的属性进行比较。使用==比较基本类型,使用
equals
比较对象。如果都相等则返回true
,否则返回false
。注意如果是在子类中定义equals
,则要包含super.equals(other)
。
下面我们给出Person
类中完整的equals
方法的书写:
@Override
public boolean equals(Object otherObject) {
// 1. 判断比较的两个对象引用是否相等,如果引用相等那么表示是同一个对象,那么当然相等
if (this == otherObject) {
return true;
}
// 2. 如果otherObject为null,直接返回false,表示不相等
if (otherObject == null ) {//对象为空或者不是Person类的实例
return false;
}
// 3. 比较this和otherObject是否是同一个类(注意下面两个只能使用一种)
// 3.1:如果equals的语义在每个子类中所有改变,就使用getClass检测
if (this.getClass() != otherObject.getClass()) {
return false;
}
// 3.2:如果所有的子类都有统一的定义,那么使用instanceof检测
if (!(otherObject instanceof Person)) {
return false;
}
// 4. 将otherObject转换成对应的类类型变量
Person other = (Person) otherObject;
// 5. 最后对对象的属性进行比较。使用==比较基本类型,使用equals比较对象。如果都相等则返回true,否则返回false
// 使用Objects工具类的equals方法防止比较的两个对象有一个为null而报错,因为null.equals()是会抛异常的
return Objects.equals(this.pname, other.pname) && this.page == other.page;
// 6. 注意如果是在子类中定义equals,则要包含super.equals(other)
// return super.equals(other) && Objects.equals(this.pname, other.pname)
// && this.page == other.page;
}
请注意,无论何时重写此方法,通常都必须重写hashCode
方法,以维护hashCode
方法的一般约定,该方法声明相等对象必须具有相同的哈希代码。hashCode
也是Object
类中的方法,后面会详细讲解。
3.3 getClass方法
上面我们在介绍equals
方法时,介绍如果equals
的语义在每个子类中有所改变,那么使用getClass
检测,为什么这样说呢?
getClass()
在Object
类中如下,作用是返回对象的运行时类。
1 | public final native Class getClass(); |
---|
这是一个用native
关键字修饰的方法,这里我们要知道用native
修饰的方法我们不用考虑,由操作系统帮我们实现,该方法的作用是返回一个对象的运行时类,通过这个类对象我们可以获取该运行时类的相关属性和方法。也就是Java
中的反射,各种通用的框架都是利用反射来实现的,这里我们不做详细的描述。
这里详细的介绍getClass
方法返回的是一个对象的运行时类对象,这该怎么理解呢?Java
中还有一种这样的用法,通过类名.class获取这个类的类对象,这两种用法有什么区别呢?
父类:Parent.class
public class Parent {}
子类:Son.class
public class Son extends Parent {}
测试:
@Test
public void testClass() {
Parent p = new Son();
System.out.println(p.getClass());
System.out.println(Parent.class);
}
打印结果:
class com.test.Son
class com.test.Parent
结论:class
是一个类的属性,能获取该类编译时的类对象,而getClass()
是一个类的方法,它是获取该类运行时的类对象。
还有一个需要大家注意的是,虽然Object
类中getClass()
方法声明是:public final native Class getClass();返回的是一个Class,但是如下是能通过编译的:
Class<? extends String> c = "".getClass();
也就是说类型为T
的变量getClass
方法的返回值类型其实是Class<? extends T>
而非getClass
方法声明中的Class<?>
。
这在官方文档中也有说明:https://docs.oracle.com/javase/8/docs/api/java/lang/Object.html#getClass--
3.4 hashCode方法
hashCode
在Object
类中定义如下:
public native int hashCode();
这也是一个用native
声明的本地方法,作用是返回对象的散列码,是int
类型的数值。
那么这个方法存在的意义是什么呢?
我们知道在Java
中有几种集合类,比如List
,Set
,还有Map
,List
集合一般是存放的元素是有序可重复的,Set
存放的元素则是无序不可重复的,而Map
集合存放的是键值对。
前面我们说过判断一个元素是否相等可以通过equals
方法,没增加一个元素,那么我们就通过equals
方法判断集合中的每一个元素是否重复,但是如果集合中有10000
个元素了,但我们新加入一个元素时,那就需要进行10000
次equals
方法的调用,这显然效率很低。
于是,Java
的集合设计者就采用了哈希表来实现。哈希算法也称为散列算法,是将数据依特定算法产生的结果直接指定到一个地址上。这个结果就是由hashCode
方法产生。这样一来,当集合要添加新的元素时,先调用这个元素的hashCode
方法,就一下子能定位到它应该放置的物理位置上。
- 如果这个位置上没有元素,它就可以直接存储在这个位置上,不用再进行任何比较了;
- 如果这个位置上已经有元素了,就调用它的
equals
方法与新元素进行比较,相同的话就不存了; - 不相同的话,也就是发生了
Hash key
相同导致冲突的情况,那么就在这个Hash key
的地方产生一个链表,将所有产生相同HashCode
的对象放到这个单链表上去,串在一起(很少出现)。这样一来实际调用equals
方法的次数就大大降低了,几乎只需要一两次。
这里有A
,B
,C
,D
四个对象,分别通过hashCode
方法产生了三个值,注意A
和B
对象调用hashCode
产生的值是相同的,即A.hashCode() = B.hashCode() = 0x001
,发生了哈希冲突,这时候由于最先是插入了A
,在插入的B
的时候,我们发现B
是要插入到A
所在的位置,而A
已经插入了,这时候就通过调用equals
方法判断A
和B
是否相同,如果相同就不插入B
,如果不同则将B
插入到A
后面的位置。所以对于equals
方法和hashCode
方法有如下要求:
3.4.1 hashCode要求
①、在程序运行时期间,只要对象的(字段的)变化不会影响equals
方法的决策结果,那么,在这个期间,无论调用多少次hashCode
,都必须返回同一个散列码。
②、通过equals
调用返回true
的2
个对象的hashCode
一定一样。
③、通过equals
返回false
的2
个对象的散列码不需要不同,也就是他们的hashCode
方法的返回值允许出现相同的情况。
因此我们可以得到如下推论:
- 两个对象相等,其
hashCode
一定相同; - 两个对象不相等,其
hashCode
有可能相同; hashCode
相同的两个对象,不一定相等;hashCode
不相同的两个对象,一定不相等;
这四个推论通过上图可以更好的理解。
可能会有人疑问,对于不能重复的集合,为什么不直接通过hashCode
对于每个元素都产生唯一的值,如果重复就是相同的值,这样不就不需要调用equals
方法来判断是否相同了吗?
实际上对于元素不是很多的情况下,直接通过hashCode
产生唯一的索引值,通过这个索引值能直接找到元素,而且还能判断是否相同。比如数据库存储的数据,ID
是有序排列的,我们能通过ID
直接找到某个元素,如果新插入的元素ID
已经有了,那就表示是重复数据,这是很完美的办法。但现实是存储的元素很难有这样的ID
关键字,也就很难这种实现hashCode
的唯一算法,再者就算能实现,但是产生的hashCode
码是非常大的,这会大的超过Java
所能表示的范围,很占内存空间,所以也是不予考虑的。
3.4.2 hashCode编写指导
①、不同对象的hash
码应该尽量不同,避免hash
冲突,也就是算法获得的元素要尽量均匀分布。
②、hash
值是一个int
类型,在Java
中占用4
个字节,也就是232次方,要避免溢出。
在JDK
的Integer
类,Float
类,String
类等都重写了hashCode
方法,我们自定义对象也可以参考这些类来写。
下面是JDK String
类的hashCode
源码:
public int hashCode() {
int h = hash;
if (h == 0 && value.length > 0) {
char val[] = value;
for (int i = 0; i < value.length; i++) {
h = 31 * h + val[i];
}
hash = h;
}
return h;
}
再次提醒大家,对于Map
集合,我们可以选取Java
中的基本类型,还有引用类型String
作为key
,因为它们都按照规范重写了equals
方法和hashCode
方法。但是如果你用自定义对象作为key
,那么一定要覆写equals
方法和hashCode
方法,不然会有意想不到的错误产生。
3.5 toString方法
该方法在JDK的源码如下:
public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
getClass().getName()
是返回对象的全类名(包含包名),Integer.toHexString(hashCode())
是以16
进制无符号整数形式返回此哈希码的字符串表示形式。
打印某个对象时,默认是调用toString
方法,比如System.out.println(person)
,等价于System.out.println(person.toString())
3.6 notify/wait方法
这是用于多线程之间的通信方法,可以参考多线程。
3.7 finalize方法
protected void finalize() throws Throwable { }
该方法用于垃圾回收,一般由JVM
自动调用,一般不需要程序员去手动调用该方法。
Java
允许在类中定义一个名为finalize()
的方法。它的工作原理是:一旦垃圾回收器准备好释放对象占用的存储空间,将首先调用其finalize()
方法。并且在下一次垃圾回收动作发生时,才会真正回收对象占用的内存。
关于垃圾回收,有三点需要记住:
- 对象可能不被垃圾回收。只要程序没有濒临存储空间用完的那一刻,对象占用的空间就总也得不到释放。
- 垃圾回收并不等于“析构”。
- 垃圾回收只与内存有关。使用垃圾回收的唯一原因是为了回收程序不再使用的内存。
finalize()
的用途:
无论对象是如何创建的,垃圾回收器都会负责释放对象占据的所有内存。这就将对finalize()
的需求限制到一种特殊情况,即通过某种创建对象方式以外的方式为对象分配了存储空间。
不过这种情况一般发生在使用“本地方法”的情况下,本地方法是一种在Java中调用非Java
代码的方式。
3.8 registerNatives方法
该方法在Object
类中定义如下:
private static native void registerNatives();
这是一个native
本地方法,想要调用操作系统的实现,必须还要装载本地库,但是我们发现在Object.class
类中具有很多本地方法,但是却没有看到本地库的载入代码。而且这是用private
关键字声明的,在类外面根本调用不了,我们接着往下看关于这个方法的类似源码:
static {
registerNatives();
}
看到上面的代码,这就明白了吧。静态代码块就是一个类在初始化过程中必定会执行的内容,所以在类加载的时候是会执行该方法的,通过该方法来注册本地方法。
3.9 clone方法
保护方法,实现对象的浅复制,只有实现了Cloneable
接口才可以调用该方法,否则抛出CloneNotSupportedException
异常。
主要是JAVA
里除了8
种基本类型传参数是值传递,其他的类对象传参数都是引用传递,我们有时候不希望在方法里讲参数改变,这是就需要在类中复写clone
方法(实现深复制)。
protected native Object clone() throws CloneNotSupportedException;
创建并返回此对象的一个副本。“副本”的准确含义可能依赖于对象的类。
3.9.1 clone与copy的区别
假设现在有一个Employee
对象tobby
,将值赋给cindyelf
,如下:
Employee tobby = new Employee("CMTobby", 5000);
// 1. 赋值
Employee cindyelf = tobby;
// 2. clone
Employee cindy = tobby.clone();
这个时候只是简单了copy
了一下reference
,cindyelf
和tobby
都指向内存中同一个object
,这样cindyelf
或者tobby
的一个操作都可能影响到对方。打个比方,如果我们通过cindyelf.raiseSalary()
方法改变了salary
域的值,那么tobby
通过getSalary()
方法得到的就是修改之后的salary
域的值,显然这不是我们愿意看到的。
我们希望得到tobby
的一个精确拷贝,同时两者互不影响,这时候我们就可以使用Clone
来满足我们的需求。clone
会生成一个新的Employee
对象,并且和tobby
具有相同的属性值和方法。
3.9.2 Shallow Clone与Deep Clone
Clone
是如何完成的呢?Object
在对某个对象实施Clone
时对其是一无所知的,它仅仅是简单地执行域对域的copy
,这就是Shallow Clone
。这样,问题就来了咯。
以Employee
为例,它里面有一个域hireDay
不是基本数据类型的变量,而是一个reference
变量,经过Clone
之后就会产生一个新的Date
型的reference
,它和原始对象中对应的域指向同一个Date
对象,这样克隆类就和原始类共享了一部分信息,而这样显然是不利的,过程下图所示:
这个时候我们就需要进行deep Clone
了,对那些非基本型别的域进行特殊的处理,例如本例中的hireDay
。我们可以重新定义Clone
方法,对hireDay
做特殊处理,如下代码所示:
class Employee implements Cloneable {
public Object clone() throws CloneNotSupportedException {
Employee cloned = (Employee) super.clone();
cloned.hireDay = (Date) hireDay.clone()
return cloned;
}
}
3.9.3 clone方法的保护机制
在Object
中Clone()
是被声明为protected
的,这样做是有一定的道理的,以Employee
类为例,通过声明为protected
,就可以保证只有Employee
类里面才能“克隆”Employee
对象。
3.9.4 clone方法的使用
Clone()
方法的使用比较简单,注意如下几点即可:
- 什么时候使用
shallow Clone
,什么时候使用deep Clone
,这个主要看具体对象的域是什么性质的,基本型别还是reference variable
。 - 调用
Clone()
方法的对象所属的类(Class
)必须implements Cloneable
接口,否则在调用Clone
方法的时候会抛出CloneNotSupportedException
。
四、为什么java.lang包下的类不需要手动导入?
不知道大家注意到没,我们在使用诸如Date
类时,需要手动导入import java.util.Date
,再比如使用File
类时,也需要手动导入import java.io.File
。但是我们在使用Object
类,String
类,Integer
类等不需要手动导入,而能直接使用,这是为什么呢?
这里先告诉大家一个结论:使用java.lang
包下的所有类,都不需要手动导入。
另外我们介绍一下Java
中的两种导包形式,导包有两种方法:
- 单类型导入(
single-type-import
),例如:import java.util.Date
。 - 按需类型导入(
type-import-on-demand
),例如:import java.util.*
。
单类型导入比较好理解,我们编程所使用的各种工具默认都是按照单类型导包的,需要什么类便导入什么类,这种方式是导入指定的public
类或者接口;
按需类型导入,比如import java.util.*
,可能看到后面的*,大家会以为是导入java.util
包下的所有类,其实并不是这样,我们根据名字按需导入要知道他是按照需求导入,并不是导入整个包下的所有类。
Java
编译器会从启动目录(bootstrap)
,扩展目录(extension)
和用户类路径下去定位需要导入的类,而这些目录进仅仅是给出了类的顶层目录,编译器的类文件定位方法大致可以理解为如下公式:
1 | 顶层路径名 \ 包名 \ 文件名.class = 绝对路径 |
---|
单类型导入我们知道包名和文件名,所以编译器可以一次性查找定位到所要的类文件。按需类型导入则比较复杂,编译器会把包名和文件名进行排列组合,然后对所有的可能性进行类文件查找定位。例如:
12345 | package com; import java.io.; import java.util.; |
---|
如果我们文件中使用到了File
类,那么编译器会根据如下几个步骤来进行查找File
类:
- File // File类属于无名包,就是说File类没有package语句,编译器会首先搜索无名包
- com.File // File类属于当前包,就是我们当前编译类的包路径
- java.lang.File // 由于编译器会自动导入java.lang包,所以也会从该包下查找
- java.io.File
- java.util.File
- ...
需要注意的地方就是,编译器找到java.io.File
类之后并不会停止下一步的寻找,而要把所有的可能性都查找完以确定是否有类导入冲突。假设此时的顶层路径有三个,那么编译器就会进行3*5=15次查找。
如果在查找完成后,编译器发现了两个同名的类,那么就会报错。要删除你不用的那个类,然后再编译。
所以我们可以得出这样的结论:按需类型导入是绝对不会降低Java
代码的执行效率的,但会影响到Java
代码的编译速度。所以我们在编码时最好是使用单类型导入,这样不仅能提高编译速度,也能避免命名冲突。
讲清楚Java
的两种导包类型了,我们在回到为什么可以直接使用Object
类,看到上面查找类文件的第③步,编译器会自动导入java.lang
包,那么当然我们能直接使用了。至于原因,因为用的多,提前加载了,省资源。