JDK1.8 --- Object类的HashCode、equals、clone方法解析

一、public native int hashCode()

       返回当前对象运行时的hash码。(在jdk源码中的解释是用于支持散列表数据结构,因为散列表在进行数据存储时依赖hash码决定数据存储的位置(逻辑位置)。在程序运行中,无论什么情况下,相同的对象对应的hash码一定是相同的。但是不同的对象有可能会返回相同的hash码。那么其实也代表如果两个对象的hash码不一致,这两个对象一定是不同的。)
这里我们做一下延伸:

为什么需要Hash码?
       Hash码的作用:文本校验,数字签名。

hash码到底是什么?(这里我们引用百度百科的内容)
       Hash译作“散列”,就是把任意长度的输入(又叫做预映射pre-image)通过散列算法变换成固定长度的输出,该输出就是散列值。这种转换是一种压缩映射,也就是,散列值的空间通常远小于输入的空间,不同的输入可能会散列成相同的输出,所以不可能从散列值来确定唯一的输入值。简单的说就是一种将任意长度的消息压缩到某一固定长度的消息摘要的函数。

以上有一些概念需要一一作出解释:
常见的散列算法:

  1. 余数法:先估计整个哈希表中的表项目数目大小。然后用这个估计值作为除数去除每个原始值,得到商和余数。用余数作为哈希值。因为这种方法产生冲突的可能性相当大,因此任何搜索算法都应该能够判断冲突是否发生并提出取代算法。
  2. 折叠法:这种方法是针对原始值为数字时使用,将原始值分为若干部分,然后将各部分叠加,得到的最后四个数字(或者取其他位数的数字都可以)来作为哈希值。
  3. 基数转换法:当原始值是数字时,可以将原始值的数制基数转为一个不同的数字。例如,可以将十进制的原始值转为十六进制的哈希值。为了使哈希值的长度相同,可以省略高位数字。
  4. 数据重排法:这种方法只是简单的将原始值中的数据打乱排序。比如可以将第三位到第六位的数字逆序排列,然后利用重排后的数字作为哈希值。

Java中Hashcode方法的规定:

  • 在 Java 应用程序执行期间,在对同一对象多次调用 hashCode 方法时,必须一致地返回相同的整数,前提是将对象进行 equals 比较时所用的信息没有被修改。从某一应用程序的一次执行到同一应用程序的另一次执行,该整数无需保持一致。

  • 如果根据 equals(Object) 方法,两个对象是相等的,那么对这两个对象中的每个对象调用 hashCode 方法都必须生成相同的整数结果。

  • 如果根据 equals(java.lang.Object) 方法,两个对象不相等,那么对这两个对象中的任一对象上调用 hashCode方法不 要求一定生成不同的整数结果。但是,程序员应该意识到,为不相等的对象生成不同整数结果可以提高哈希表的性能。

Hotspot中hashcode方法的实现:


图片 1.png

       hash方法的实现是先获取该对象的标记字对象,然后对该标记字对象的的地址做位移和逻辑与操作,以结果作为hashcode(其中,mark_bits方法在globalDefinitions.hpp),之所以做移位操作是因为hashcode在标记字中只占用了部分位(32位机器上是占用25位,64位机器上占用31)
       从实现来看,我们试图解释hashcode的一个现象,为什么不同的对象可能返回相同的hashcode方法?
因为尽管虚拟机在运行过程中,不同的对象的地址一定是不同的,但是由于hashcode需要固定25位或者31位,那么就导致真正的hashcode值需要在对象地址上做一定的操作。从而将一个大范围区间的值映射到一个小范围区间的值(hashcode的计算过程),这样的操作必定会导致一部分数据的计算结果会重复。所以说这就是不同的对象可能返回相同hash值的原因。

二、 public boolean equals(Object obj)

图片 1.png

在java中equals的作用是,判断两个对象是不是相等的。
       从jdk的实现可以看出:equals其实就是在比较两个对象的地址是否相等,所以这个方法不存在hashcode()的问题,因为对象在内存中的地址是唯一的。
重写equals()方法就必须重写hashCode()方法的原因。
       假设两个对象,重写了其equals方法,其相等条件是属性相等,就返回true。如果不重写hashcode方法,其返回的依然是两个对象的内存地址值,必然不相等。这就出现了equals方法相等,但是hashcode不相等的情况。这不符合hashcode的规则。在集合框架中,这种情况会导致的严重的问题,具体的问题在hashMap中会具体分析。

三、 protected native Object clone()

本地方法表
static JNINativeMethod methods[] = {
       {"hashCode", "()I", (void *)&JVM_IHashCode},
       {"wait", "(J)V", (void *)&JVM_MonitorWait},
       {"notify", "()V", (void *)&JVM_MonitorNotify},
       {"notifyAll", "()V", (void *)&JVM_MonitorNotifyAll},
       {"clone", "()Ljava/lang/Object;", (void *)&JVM_Clone}
};
       由本地方法表知道clone方法对应的本地函数为JVM_Clone,clone方法主要实现对象的克隆功能,根据该对象生成一个相同的新对象(我们常见的类的对象的属性如果是原始类型则会克隆值,但如果是对象则会克隆对象的地址)。Java的类要实现克隆则需要实现Cloneable接口,if (!klass->is_cloneable())这里会校验是否有实现该接口。然后判断是否是数组分两种情况分配内存空间,新对象为new_obj,接着对new_obj进行copy及C++层数据结构的设置。最后再转成jobject类型方便转成Java层的Object类型。
源码:


图片 1.png

看到jvm源码之后我们来分析一下:
       底层在实现clone()方法其实非常简单,一.首先确认当前对象是否实现了cloneable接口(这是一个标记接口),二.分配一个和当前对象一样大小的空间,三.将原对象堆中的区域以字节的方式复制到新对象中。
       然而对象在堆中的存储,对象属性其实存储的只是引用地址,那么使用clone()方法的时候就是我们所说浅拷贝,只是值拷贝。拷贝的接口是拷贝的对象和原对象的对象属性指向同一个地址。对这个属性做改动时,会互相影响。

那么引出一个问题,我们怎么进行深拷贝呢?
       解决的方法是调用对象属性的clone()方法,需要所有的对象属性都重写clone()方法。然后再调用上层对象的clone()方法时,再里面调用属性的对象的clone()方法,实现深拷贝。

还有一个值得思考的问题,String对象如何进行拷贝的?
       String对象本身是个final类,所以在对它做拷贝的时候注定是值拷贝,拷贝之后的String和原来的String指向同一个区域(常量池),然后String有一个基本特性是它的不可变性,如果改变就新增,这样的话虽然是浅拷贝,但变相的实现了深拷贝的效果。

你可能感兴趣的:(JDK1.8 --- Object类的HashCode、equals、clone方法解析)