常见面试题:Object类中有哪些方法,分别有什么作用?

常见面试题:Object类中有哪些方法,分别有什么作用?

getClass

public final native Class<?> getClass();

这是一个 final 方法,我们在子类中无法覆写,通过调用这个方法可以得到实例对象所属类型对应的 Class 对象

hashCode

public native int hashCode();

这个方法算出来的哈希值是存放在对象头的 markword 中的(第一次调用时计算,后续不会再改动),哈希表工作时,就是根据对象的哈希值(再经过几步运算)来决定把这个对象放在哪个链表中,如果我们不覆写,那么是由一个 native 方法计算出,并且根据给定虚拟机参数的不同有不同的计算策略:

//通过"-XX:hashCode=4"可以设置hashCode方法的计算策略
//注意,有些策略是实验性的,要先通过"-XX:+UnlockExperimentalVMOptions"来允许使用这些策略

if (hashCode == 0) {
    //一个随机数
    value = os::random();
} else if (hashCode == 1) {
    //在对象地址的基础上做一些运算
    intptr_t addrBits = cast_from_oop<intptr_t>(obj) >> 3;
    value = addrBits ^ (addrBits >> 5) ^ GVars.stwRandom;
} else if (hashCode == 2) {
    //固定值1
    value = 1;     
} else if (hashCode == 3) {
    //一个自增的值
    value = ++GVars.hcSequence;
} else if (hashCode == 4) {
    //对象地址
    value = cast_from_oop<intptr_t>(obj);
} else {
    //JDK8默认的策略
    //涉及四个数字,一个是和线程相关的随机数,另外三个是常量,经过运算后得出哈希值
    unsigned t = Self->_hashStateX;
    t ^= (t << 11);
    Self->_hashStateX = Self->_hashStateY;
    Self->_hashStateY = Self->_hashStateZ;
    Self->_hashStateZ = Self->_hashStateW;
    unsigned v = Self->_hashStateW;
    v = (v ^ (v >> 19)) ^ (t ^ (t >> 8));
    Self->_hashStateW = v;
    value = v;
}

equals

public boolean equals(Object obj) {
    return (this == obj);
}

这个方法是用来比较两个对象是否相同的,我们可以在子类中覆写这个方法,从而让对象按照我们的想法来比较是否相同

对于基本数据类型,我们可以直接用 == 来判断值是否相同,而对于引用类型,用 == 比较的是两个对象的内存地址是否相同

观察 Object 类提供的 equals 方法的默认实现,可以发现就是调用了 ==,因此,如果我们不为子类覆写这个方法,那么无论用 == 还是 equals 方法其实比较的都是内存地址,如果我们想要自定义比较的方式,一定要覆写这个方法

此外,需要注意的是,如果我们覆写了 equals 方法,一般也要把 hashCode 方法一起覆写了,并且要满足两个准则:

  • 如果两个对象的 equals 结果为 true,那么这两个对象的哈希值一定得相同(保证相同元素一定得在同一个链表里)
  • 如果两个对象的哈希值相同,并不要求两个对象的 equals 结果一定为 true(因为同一个链表里的元素不一定相同)

如果两个方法都不覆写,其实天然就是满足这两个准则的:

  • 如果 equals 返回 true,说明内存地址相同,也就是说这两个引用指向的其实是同一个对象,同一个对象的哈希值当然是一样的
  • 如果哈希值相同,只能说明比较巧,因为默认的哈希算法是随机的,两个地址不同的对象算出同一个哈希值也不是不可能

试想,如果只覆写了 equals 方法,假如现在有 person1person2 两个相同的对象(内存地址不同,但 equals 返回 true),我们先把 person1:999 放到哈希表中,理论上我们可以通过 person2 来 get 到 999,但实际上,由于 hashCode 方法还是使用的默认的随机数,因此 person1 和 person2 的哈希值大概率是不同的,因此 person1 可能会被放到 table[10] 的位置,而在根据 person2 查找的时候可能会到 table[3] 的地方去找,那肯定是找不到的,只有保证哈希值相同,那么在根据 person2 查找时才会也到 table[10] 的位置进行查找,这才满足了我们使用哈希表的本意

clone

protected native Object clone() throws CloneNotSupportedException;

首先,要是想让某个类的实例对象支持 clone 操作,需要让这个类继承 Cloneable 接口,并且覆写 clone 这个方法,最简单的一个实现是这样子的:

class Person implements Cloneable {
    public String name;
    public Integer age;

    public Person(String name, Integer age) {
        this.name = name;
        this.age = age;
    }

    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}

但这样 clone 出来的对象有个问题,虽然这确实是两个对象(内存地址不同),但它们的 name 却还是同一个对象:

public class Test {
    public static void main(String[] args) throws CloneNotSupportedException {
        Person p1 = new Person("chen", 20);
        Person p2 = (Person) p1.clone();
        //false
        System.out.println(p1 == p2);
        //true
        System.out.println(p1.name == p2.name);
    }
}

这其实就是一个浅拷贝,如果想要完成深拷贝,即想要让 clone 出来的对象的属性也是独一份的,此时 clone 方法就不能写那么简单了:

class Body implements Cloneable {
    public Head head;

    public Body(Head head) {
        this.head = head;
    }

    @Override
    protected Object clone() throws CloneNotSupportedException {
        Body newBody = (Body) super.clone();
        //属性也得是clone出来的
        newBody.head = (Head) this.head.clone();
        return newBody;
    }

}

这里想要 clone 一个 Body 对象,如果按照之前写法,虽然能得到一个新的 Body 对象,但这两个对象的 Head 还是同一份,因此,我们需要在 Body 的 clone 方法中把 Head 也 clone 一份,并且交给新的 Body 对象,这样一来得到的 Body 对象就是一个深拷贝的结果了

但需要注意的是,这样写有两点要求:

  • Head 这个类型也得实现了自己 clone 方法
  • Head 这个类中不再有其它的引用属性,如果有,比如 Face 这个引用属性,那么就得按同样的道理让 Face 也实现 clone 方法,并在 Head 的 clone 中让这个 Face 也是 clone 出来的,否则这是一个不彻底的深拷贝

下面三张图依次展示了浅拷贝、深拷贝、不彻底的深拷贝:

常见面试题:Object类中有哪些方法,分别有什么作用?
常见面试题:Object类中有哪些方法,分别有什么作用?
常见面试题:Object类中有哪些方法,分别有什么作用?

toString

public String toString() {
	return getClass().getName() + "@" + Integer.toHexString(hashCode());
}

把当前对象转变为一个字符串,在默认的实现中,这个字符串的内容为当前类的类名以及该实例对象的哈希值

大多数情况下我们都会对该方法进行覆写,从而转换为一个能体现出对象信息的字符串

wait

public final native void wait(long timeoutMillis) throws InterruptedException;

假如有多个线程在并发访问某个对象,现在线程 A 成功获取这个对象的锁,此时如果 A 执行了 wait 方法,就会让自己进入等待状态,并且释放对象的锁,让其它线程去竞争

注意,对于一个等待中的线程,就算对象的锁是空闲的,也不会去竞争,需要区别于阻塞状态,阻塞中的线程一旦发现对象的锁空闲就会去竞争

notify

public final native void notify();

还是前面的场景,现在线程 A B C 都处于等待状态,对象的锁在线程 D 手中,此时如果 D 执行了 notify 方法,就会随机唤醒一个等待中的线程,假如唤醒了 A,那么 A 就会从等待状态进入阻塞状态,在 D 释放锁之后,线程 A 会和阻塞中的其它线程一起竞争这个锁,但等待状态下的 B C 不会参与竞争

notifyAll

public final native void notifyAll();

notifyAll 和 notify 的唯一区别在于,notify 是随机唤醒一个等待中的线程,而 notifyAll 是唤醒所有等待中的线程,其它都是一样的

finalize

protected void finalize() throws Throwable { }

这个实例方法只有在以下三个条件都满足时才会触发:

  • 在可达性分析中,垃圾收集器发现该对象与 GC Roots 不可达
  • 该对象所属类覆写了 finalize 方法
  • 该对象的 finalize 方法还未触发过

因此,这个实例方法是在对象将要被回收时才会触发的,理论上可以在这个方法里做两件事情:

  • 对象自救,即在方法中重新把自己和某个引用联系起来,从而避免被回收
    • 个人认为是一个比较鸡肋的功能
  • 做一些资源的释放,比如文件流的关闭等等
    • 虽然可以,但不可靠,因为虚拟机只会让这个方法开始执行,但不保证会等它执行完毕(以免占用太多时间,影响其它垃圾的回收工作),所以可能没等资源释放完虚拟机就不管了,因此,如果真的要释放资源的话还不如放在 finally 代码块中,没必要写在这个方法里

综上,finalize 是一个没什么用的方法,从 JDK9 开始这个方法也被标识为了 Deprecated,建议不要使用这个方法

学习过程中,部分内容参考了网上的文章,如有侵权必删

常见面试题:Object类中有哪些方法,分别有什么作用?

上一篇:Revit几何操作集合


下一篇:reactive, toRefs 用法