Java常用类之Object源码分析

一、概述

理论上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关键字调用构造器完成对象的实例化,还能通过构造器对对象进行相应的初始化。一个类必须要有一个构造器的存在,如果没有显示声明,那么系统会默认创造一个无参构造器,在JDKObject类源码中,是看不到构造器的,系统会自动添加一个无参构造器。

我们可以通过以下构造一个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方法的使用必须遵循以下几个原则:

  1. 自反性:对于任何非空引用值x,x.equals(x)都应返回true。
  2. 对称性:对于任何非空引用值x和y,当且仅当y.equals(x)返回true时,x.equals(y)才应返回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,前提是对象上equals比较中所用的信息没有被修改
  5. 对于任何非空引用值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关键字上,ManPerson的子类,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)得到的结果是falseman.equals(person)得到的结果也是false,满足对称性。

注意:使用getClass不是绝对的,要根据情况而定,毕竟定义对象是否相等的标准是由程序员自己定义的。而且使用getClass不符合多态的定义,比如AbstractSet抽象类,它有两个子类TreeSetHashSet,他们分别使用不同的算法实现查找集合的操作,但无论集合采用哪种方式实现,都需要拥有对两个集合进行比较的功能,如果使用getClass实现equals方法的重写,那么就不能在两个不同子类的对象进行相等的比较。而且集合类比较特殊,其子类是不需要自定义相等的概念的。

所以什么时候使用instanceof运算符,什么时候使用getClass()有如下建议:

①、如果子类能够拥有自己的相等概念,则对称性需求将强制采用getClass进行检测。
②、如果有超类决定相等的概念,那么就可以使用instanceof进行检测,这样可以在不同的子类的对象之间进行相等的比较。

下面给出一个完美的equals方法的建议:

  1. 显示参数命名为otherObject,稍后会将它转换成另一个叫做other的变量。
  2. 判断比较的两个对象引用是否相等,如果引用相等那么表示是同一个对象,那么当然相等。
  3. 如果otherObjectnull,直接返回false,表示不相等。
  4. 比较thisotherObject是否是同一个类:如果equals的语义在每个子类中有所改变,就使用getClass检测;如果所有的子类都有统一的定义,那么使用instanceof检测。
  5. otherObject转换成对应的类类型变量。
  6. 最后对对象的属性进行比较。使用==比较基本类型,使用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方法

hashCodeObject类中定义如下:

public native int hashCode();

这也是一个用native声明的本地方法,作用是返回对象的散列码,是int类型的数值。

那么这个方法存在的意义是什么呢?

我们知道在Java中有几种集合类,比如ListSet,还有MapList集合一般是存放的元素是有序可重复的,Set存放的元素则是无序不可重复的,而Map集合存放的是键值对。

前面我们说过判断一个元素是否相等可以通过equals方法,没增加一个元素,那么我们就通过equals方法判断集合中的每一个元素是否重复,但是如果集合中有10000个元素了,但我们新加入一个元素时,那就需要进行10000equals方法的调用,这显然效率很低。

于是,Java的集合设计者就采用了哈希表来实现。哈希算法也称为散列算法,是将数据依特定算法产生的结果直接指定到一个地址上。这个结果就是由hashCode方法产生。这样一来,当集合要添加新的元素时,先调用这个元素的hashCode方法,就一下子能定位到它应该放置的物理位置上。

  1. 如果这个位置上没有元素,它就可以直接存储在这个位置上,不用再进行任何比较了;
  2. 如果这个位置上已经有元素了,就调用它的equals方法与新元素进行比较,相同的话就不存了;
  3. 不相同的话,也就是发生了Hash key相同导致冲突的情况,那么就在这个Hash key的地方产生一个链表,将所有产生相同HashCode的对象放到这个单链表上去,串在一起(很少出现)。这样一来实际调用equals方法的次数就大大降低了,几乎只需要一两次。

这里有A,B,C,D四个对象,分别通过hashCode方法产生了三个值,注意AB对象调用hashCode产生的值是相同的,即A.hashCode() = B.hashCode() = 0x001,发生了哈希冲突,这时候由于最先是插入了A,在插入的B的时候,我们发现B是要插入到A所在的位置,而A已经插入了,这时候就通过调用equals方法判断AB是否相同,如果相同就不插入B,如果不同则将B插入到A后面的位置。所以对于equals方法和hashCode方法有如下要求:

3.4.1 hashCode要求

①、在程序运行时期间,只要对象的(字段的)变化不会影响equals方法的决策结果,那么,在这个期间,无论调用多少次hashCode,都必须返回同一个散列码。
②、通过equals调用返回true2个对象的hashCode一定一样。
③、通过equals返回false2个对象的散列码不需要不同,也就是他们的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次方,要避免溢出。

JDKInteger类,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()方法。并且在下一次垃圾回收动作发生时,才会真正回收对象占用的内存。

关于垃圾回收,有三点需要记住:

  1. 对象可能不被垃圾回收。只要程序没有濒临存储空间用完的那一刻,对象占用的空间就总也得不到释放。
  2. 垃圾回收并不等于“析构”。
  3. 垃圾回收只与内存有关。使用垃圾回收的唯一原因是为了回收程序不再使用的内存。

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了一下referencecindyelftobby都指向内存中同一个object,这样cindyelf或者tobby的一个操作都可能影响到对方。打个比方,如果我们通过cindyelf.raiseSalary()方法改变了salary域的值,那么tobby通过getSalary()方法得到的就是修改之后的salary域的值,显然这不是我们愿意看到的。

我们希望得到tobby的一个精确拷贝,同时两者互不影响,这时候我们就可以使用Clone来满足我们的需求。clone会生成一个新的Employee对象,并且和tobby具有相同的属性值和方法。

3.9.2 ShallowClone与DeepClone

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方法的保护机制

ObjectClone()是被声明为protected的,这样做是有一定的道理的,以Employee类为例,通过声明为protected,就可以保证只有Employee类里面才能“克隆”Employee对象。

3.9.4 clone方法的使用

Clone()方法的使用比较简单,注意如下几点即可:

  1. 什么时候使用shallow Clone,什么时候使用deep Clone,这个主要看具体对象的域是什么性质的,基本型别还是reference variable
  2. 调用Clone()方法的对象所属的类(Class)必须implements Cloneable接口,否则在调用Clone方法的时候会抛出CloneNotSupportedException

四、拓展

4.1 为什么java.lang包下的类不需要手动导入?

不知道大家注意到没,我们在使用诸如Date类时,需要手动导入import java.util.Date,再比如使用File类时,也需要手动导入import java.io.File。但是我们在使用Object类,String类,Integer类等不需要手动导入,而能直接使用,这是为什么呢?

这里先告诉大家一个结论:使用java.lang包下的所有类,都不需要手动导入。

另外我们介绍一下Java中的两种导包形式,导包有两种方法:

  1. 单类型导入(single-type-import),例如:import java.util.Date
  2. 按需类型导入(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类:

  1. File        // File类属于无名包,就是说File类没有package语句,编译器会首先搜索无名包
  2. com.File     // File类属于当前包,就是我们当前编译类的包路径
  3. java.lang.File    // 由于编译器会自动导入java.lang包,所以也会从该包下查找
  4. java.io.File
  5. java.util.File
  6. ...

需要注意的地方就是,编译器找到java.io.File类之后并不会停止下一步的寻找,而要把所有的可能性都查找完以确定是否有类导入冲突。假设此时的顶层路径有三个,那么编译器就会进行3*5=15次查找。

如果在查找完成后,编译器发现了两个同名的类,那么就会报错。要删除你不用的那个类,然后再编译。

所以我们可以得出这样的结论:按需类型导入是绝对不会降低Java代码的执行效率的,但会影响到Java代码的编译速度。所以我们在编码时最好是使用单类型导入,这样不仅能提高编译速度,也能避免命名冲突。

讲清楚Java的两种导包类型了,我们在回到为什么可以直接使用Object类,看到上面查找类文件的第③步,编译器会自动导入java.lang包,那么当然我们能直接使用了。至于原因,因为用的多,提前加载了,省资源。

参考文章

posted @ 2023-02-22 16:51  夏尔_717  阅读(38)  评论(0编辑  收藏  举报