在讨论hashCode之前,我先提出几个问题,后面我会一一解答
- 什么是hashCode?
- hashCode有什么用?
- 为什么会发生哈希冲突,可以避免吗?
- 为什么重写equals()方法一定要重写hashCode()方法?
- hashCode在Java中是如何生成的?
在讨论上述问题之前,我们先看看java里面是如何对hashCode定义的,下面我摘抄一段java8 Object对hashCode()的一段注释
1 | java复制代码 /** |
我把他的意思大致归纳于以下几点
- 每个对象都能返回一个hashCode,hashCode是用来加速哈希表的
- 在同一java应用程序执行期间,对同一个对象调用超过一次时,如果equals()比较的内容没有发生变化,hashCode()的返回应该也不应该发生变化。(特别需要注意的是同一java程序,不同java程序可以允许hashCode发生变化)
- 根据equals()方法,两个对象相等,那么hashCode也必须要相等
- 根据equals()方法,两个对象不相等,那么hashCode可能相等,但是要尽可能的让他们不相等,这样可以提升哈希表的性能
什么是hashCode?
hashCode是用来加速哈希表的,在同一个java应用程序中,hashCode是对java对象的一个签名,相等的对象hashCode一定相等。
hashCode有什么用?
上面也已经提到了,是用来加速哈希表的,采取了空间换时间的方式。
这一篇博客不是说哈希表的,因此这里不做展开
大致思路是先通过hashcode去哈希表里面找到对应的哈希桶,然后调用equals()方法去判断哈希桶里面挂载的元素有没有相等的。
为什么相等的元素会出现在同一个哈希桶里面?
前文已经说过了,相等的对象hashcode一定相等,hashcode相等的,对象却不一定相等,因此需要再调用equals()方法做判断
为什么会发生哈希冲突,可以避免吗?
先来看一下java里面hashCode的方法签名
1 | java复制代码public native int hashCode(); |
可以看到hashCode本质上面来说是一个int,int的表示的数据范围是有限的,而对象是无限的,把一个无限的集合压缩进一个有限的集合中,发生哈希冲突就成了一个必然事件,哈希冲突是无法避免的。
当然,在我们的应用中,对象不可能是无穷无尽的,因此选择好的哈希算法是可以降低哈希冲突的概率的
为什么重写equals()方法一定要重写hashCode()方法?
这个问题实际上前面已经多次提到了,根据java规范,equals()相等的对象,hashCode()必须相等
重写equals()必然导致对象是否相等的结果发生变化,因此也就需要修改hashCode()方法
如果只修改equals()不修改hashCode()方法,就会导致使用哈希表的时候不能准确定位到哈希桶,导致哈希表工作异常
hashCode在Java中是如何生成的?
这个问题其实很复杂,我再摘抄一段java8 Object里面的注释
1 | java复制代码 /** |
这句话的意思是说,很多时候hashcode是对象内存地址的一个映射,但是java里面不是的。
那么java里面的hashCode是如何生成的呢?
实际上java默认的hashcode是由一套随机算法生成的,只与对象生成的顺序和线程有关。
下面我用代码证明下
1 | java复制代码 public static void main(String[] args) { |
可以直接用Unsafe类拿到内存地址,不过操作比较繁琐,因此我用了别人封装的工具包
1 | xml复制代码 <dependency> |
反复执行上面代码,结果如下
第一次 | 第二次 | 第三次 |
---|---|---|
hashCode:531885035 address:31856529176 hashCode:705265961 address:31874395832 hashCode:428746855 address:31874396520 hashCode:317983781 address:31874397208 hashCode:987405879 address:31874397896 | hashCode:531885035 address:31856529344 hashCode:705265961 address:31874395856 hashCode:428746855 address:31874396544 hashCode:317983781 address:31874397232 hashCode:987405879 address:31874397920 | hashCode:531885035 address:31856529920 hashCode:705265961 address:31874407200 hashCode:428746855 address:31874407888 hashCode:317983781 address:31874408576 hashCode:987405879 address:31874409264 |
我们可以清楚的看到,相同的顺序hashCode是一样的,内存地址是不一样的,因此可以肯定java的hashCode和内存是无关的,看起来与创建顺序有关。
接下来我们用代码去查看发生哈希冲突时的情况
1 | java复制代码 public static void main(String[] args) { |
重复运行上面代码
第一次 | 第二次 | 第三次 |
---|---|---|
count:105730 hashCode:2134400190 count:111177 hashCode:651156501 count:121838 hashCode:1867750575 count:145361 hashCode:2038112324 count:146294 hashCode:1164664992 | count:105730 hashCode:2134400190 count:111177 hashCode:651156501 count:121838 hashCode:1867750575 count:145361 hashCode:2038112324 count:146294 hashCode:1164664992 | count:105730 hashCode:2134400190 count:111177 hashCode:651156501 count:121838 hashCode:1867750575 count:145361 hashCode:2038112324 count:146294 hashCode:1164664992 |
我们可以看到,发生哈希冲突的位置是一样的,发生哈希冲突时的hashCode也是一样的,因此我们可以断定hashcode的生成与对象生成的顺序有关。
接下来我们用不同的线程去重复上述操作
1 | java复制代码 public static void main(String[] args) throws InterruptedException { |
结果如下
第一次 | 第二次 | 第三次 |
---|---|---|
count:53869 hashCode:820543451 count:87947 hashCode:951498229 count:99859 hashCode:175788428 count:100940 hashCode:1979813773 count:123438 hashCode:916565907 | count:51579 hashCode:1573800482 count:124111 hashCode:1834341042 count:139761 hashCode:2021276643 count:142276 hashCode:1807321828 count:143515 hashCode:1522017140 | count:69186 hashCode:742597489 count:102967 hashCode:2084692455 count:144290 hashCode:424089733 count:176112 hashCode:53258565 count:178639 hashCode:1046986547 |
看起来没有任何规律了,这就需要去扒java的源码才能解答这个现象了
在Object中我们可以看到hashCode实际上是调用了IHashCode
1 | c复制代码static JNINativeMethod methods[] = { |
然后在jvm.cpp找到了IHashCode实际调用的是FastHashCode
1 | c复制代码JVM_ENTRY(jint, JVM_IHashCode(JNIEnv* env, jobject handle)) |
接着扒FastHashCode,FastHashCode在synchronizer.cpp中
1 | c复制代码intptr_t ObjectSynchronizer::FastHashCode (Thread * Self, oop obj) { |
get_next_hash
1 | c复制代码static inline intptr_t get_next_hash(Thread * Self, oop obj) { |
到了这里我们基本上就知道了hashcode是怎么生成的了,实际上在jdk1.8在是用的第五种生成方式,我们可以在Linux系统下输入:java -XX:+PrintFlagsFinal -version|grep hashCode命令查看
ok,接下来我们来分析一下第5种方式,看到这个代码我的第一反应是懵逼的,那个什么_hashStateX是个什么鬼?
我们来看一下thead.cpp里面是怎样定义的:
1 | c复制代码// thread-specific hashCode stream generator state - Marsaglia shift-xor form |
在thread里面定义了一个随机数,三个常数,通过这四个数根据上述的算法来生成hashcode。
具体原理请参考论文:Xorshift RNGs
因为在上述算法在,需要得到线程里面的一个随机数作为一个初始值,上述算法在前后具有因果关系,后面的结果是根据前面的结果推算而来的,因此对于相同的线程来说,在某种意义上,对象的hashcode的生成是和顺序有关的。
为什么主函数里面的hashcode的生成一直是有序的呢?因为主线程里面的是固定值。
可见hashCode在1.8中和内存地址是无关的,与所在线程(或者说生成的一个随机数)以及生成顺序有关
本文转载自: 掘金