当前位置: 首页 > 后端技术 > Java

为了彻底理解hashCode,我看了一下JDK的源码

时间:2023-04-01 19:23:34 Java

今天我们就来聊一聊Java中的hashCode()方法——从源码的角度。众所周知,Java是一门面向对象的编程语言,默认情况下所有的类都会继承自Object类,而Object在中文中就是“对象”的意思。Object类包含hashCode()方法:@HotSpotIntrinsicCandidatepublicnativeinthashCode();意味着所有类都将有一个返回int类型值的hashCode()方法。由于hashCode()方法是本地方法(由native关键字修饰,C/C++语言实现,Java调用的方法),也就是说Object类中没有给出具体实现。具体实现请参考jdk/src/hotspot/share/runtime/synchronizer.cpp(源码可在GitHub的OpenJDK仓库下载)。get_next_hash()方法会根据hashCode的值来决定使用哪种哈希值生成策略。并且hashCode()方法被@HotSpotIntrinsicCandidate注解修饰,说明它在HotSpot虚拟机中有一套高效的实现,基于CPU指令。我在Thinking中输出了100多篇关于Java的文章,总字数超过30万字。内容幽默风趣,通俗易懂,得到了众多初学者的认可和支持。编程、Java虚拟机等核心内容。为了帮助更多的Java初学者,我一气之下将这些文章重新整理并开源到GitHub,并命名为《教妹学 Java》。听起来有趣吗?GitHub开源地址(欢迎star):https://github.com/itwanger/jmx-java大家有没有想过这个问题:为什么Object类需要一个hashCode()方法?在Java中,hashCode()方法的主要功能是处理哈希表。哈希表(HashTable),也叫哈希表,是一种可以通过key-value直接访问的数据结构。它最大的特点是可以快速实现查找、插入和删除。使用的算法称为哈希,将任意长度的输入转换为固定长度的输出,输出就是哈希值。使用像MD5和SHA1这样的哈希算法。Java中的HashSet、Hashtable(注意小写的t)、HashMap都是基于哈希表的具体实现。其中,HashMap是最典型的代表。不仅面试官经常问,工作中使用频率也很高。想一想,如果没有哈希表,但是你需要这样一个数据结构,里面存储的数据不允许重复,怎么办?不使用equals()方法进行一对一比较怎么样?这个方案当然可行。但是如果数据量特别大,使用equals()方法进行一对一比较的效率肯定很低。最好的解决方案是哈希表。以HashMap为例。当我们要向其中添加一个对象时,首先调用对象的hashCode()方法获取对应的hash值,然后将hash值和对象放入HashMap中。当我们要添加一个新的对象时:获取对象的哈希值;与已有的hash值进行比较,不相等则直接存储;如果存在相等,则调用equals()方法保存对象之间的比较,如果相等则不保存;如果不相等,说明hash冲突,增加一个链表存放新的对象;如果链表长度大于8,则转为红黑树处理。这样调用equals()方法的频率就大大降低了。换句话说,只要哈希算法足够高效,能够将哈希冲突的频率降到最低,哈希表的效率就特别高。我们来看看HashMap的哈希算法:staticfinalinthash(Objectkey){inth;返回(键==空)?0:(h=key.hashCode())^(h>>>16);}先调用对象的hashCode()方法,然后对值进行右移,再进行异或运算。一般来说,String会作为HashMap的key进行hash运算,那么我们来看看String的hashCode()方法:publicinthashCode(){inth=hash;如果(h==0&&value.length>0){hash=h=isLatin1()?StringLatin1.hashCode(值):StringUTF16.hashCode(值);}returnh;}publicstaticinthashCode(byte[]value){inth=0;intlength=value.length>>1;for(inti=0;iscores=newHashMap<>();scores.put(s1,98);System.out.println(scores.get(newStudent(18,"张三")));}}classStudent{privateintage;私有字符串名称;publicStudent(intage,Stringname){this.age=age;this.name=名称;}@Overridepublicbooleanequals(Objecto){Studentstudent=(Student)o;返回年龄==student.age&&Objects.equals(name,student.name);我们覆盖了Student类的equals()方法,这样如果两个学生的年龄和名字相同,我们就认为他们是同一个学生,这很荒谬,但我们就是这么草率。在main()方法中,18岁的张三考了98分,这是一个非常好的成绩。我们把张三和分数放在HashMap中,然后准备输出张三的分数:null可惜结果是null,而不是预期的98。为什么是这样?原因是重写equals()方法时没有重写hashCode()方法。默认情况下,hashCode()方法是返回对象存储地址的本地方法。很明显,put()中的s1和get()中的newStudent(18,"张三")是两个对象,它们的存储地址肯定是不一样的。HashMap的get()方法会调用hash(key.hashCode())计算对象的哈希值。虽然两个不同的hashCode()结果经过hash()方法计算后可能得到相同的结果,但是这个概率是很小的,所以scores.get(newStudent(18,"张三")))无法得到期望值of18.如何解决这个问题?很简单,重写hashCode()方法。@OverridepublicinthashCode(){returnObjects.hash(age,name);}Objects类的hash()方法可以为不同数量的参数生成新的hashCode()值。publicstaticinthashCode(Objecta[]){if(a==null)返回0;整数结果=1;for(Objectelement:a)result=31*result+(element==null?0:element.hashCode());returnresult;}代码看似很简单,归纳出的数学公式如下(n为字符串长度)。注:31是奇质数,不能太大也不能太小。一般来说,素数非常适合哈希计算。偶数相当于移位操作,容易溢出,造成数据信息丢失。这意味着如果年龄和姓名相同,将得到相同的哈希值。scores.get(newStudent(18,"ZhangSan"))会返回期望值98。《Java 编程思想》这本圣经中有一段描述了hashCode()方法。设计hashCode()时最重要的因素是,无论何时在同一个对象上调用hashCode(),它都应该生成相同的值。如果用put()方法将对象添加到HashMap时产生了一个hashCode()值,用get()方法取出时又产生了一个hashCode()值,那么这个对象就取不出来了。所以,如果你的hashCode()方法依赖于对象中的volatile数据,用户要小心,因为当这些数据发生变化时,hashCode()会生成不同的哈希值,相当于生成不同的键。换句话说,如果对象中的某个字段在重写hashCode()和equals()方法时容易发生变化,那么最好丢弃这些字段,以避免出现不可预知的结果。好的。以上述内容为基础,我们再回顾一下本地方法hashCode()的C++源码。静态内联intptr_tget_next_hash(Thread*current,oopobj){intptr_tvalue=0;if(hashCode==0){//这种形式使用全局Park-MillerRNG。//在MP系统上,我们将对全局进行大量RW访问,因此//机制会导致大量一致性流量。价值=操作系统::随机();}elseif(hashCode==1){//这种变体具有在STW操作之间稳定(幂等)的特性。这在某些1-0//同步方案中很有用。intptr_taddr_bits=cast_from_oop(obj)>>3;value=addr_bits^(addr_bits>>5)^GVars.stw_random;}elseif(hashCode==2){value=1;//用于敏感性测试}elseif(hashCode==3){value=++GVars.hc_sequence;}elseif(hashCode==4){value=cast_from_oop(obj);}else{//Marsaglia的具有线程特定状态的异或移位方案//这可能是最好的整体implementation——我们//可能会在未来的版本中将其设为默认值。无符号t=当前->_hashStateX;t^=(t<<11);当前->_hashStateX=当前->_hashStateY;当前->_hashStateY=当前->_hashStateZ;当前->_hashStateZ=当前->_hashStateW;无符号v=current->_hashStateW;v=(v^(v>>19))^(t^(t>>8));当前->_hashStateW=v;值=v;}值&=markWord::hash_mask;如果(值==0)值=0xBAD;assert(value!=markWord::no_hash,"不变");returnvalue;}如果没有C++的基础,我们不需要细看每一行代码。我们只需要从表面上理解get_next_hash()方法即可。hashCode变量是JVM启动时的全局参数,可以通过它来切换hash值的生成策略。hashCode==0,调用操作系统OS的random()方法返回一个随机数。hashCode==1,在STW(stop-the-world)操作中,这种策略通常用在同步方案中。使用对象地址执行计算,使用不经常更新的随机数(GVars.stw_random)进行参与。hashCode==2,用于返回1,用于某些情况下的测试。hashCode==3,从0开始计算hash值,不是线程安全的,多个线程可能得到相同的hash值。hashCode==4,与创建对象的内存位置有关,原样输出。hashCode==5,默认值,支持多线程,使用Marsaglia的xor-shift算法生成伪随机数。所谓xor-shift算法,简单来说,看起来就像一个移位寄存器,每次移入的位都是通过寄存器中的几个位进行异或运算产生的。所谓伪随机数并不是完全随机的,但很难产生真随机数,所以只要能通过一定的随机数统计检验,就可以作为真随机数使用。至于更深层次的挖掘,涉及到数学知识和物理知识,就不展开了。毕竟,食物是原罪。二哥在四物上写了很多Java文章,包括Java核心语法、Java集合框架、JavaIO、Java并发编程、Java虚拟机等等,可以算是一个完整的体系。为了帮助更多的Java初学者,二哥将他的连载开源到GitHub。他虽然只整理了50篇文章,却发现字数已经达到了10万+,而且内容没话说,通俗易懂,幽默风趣,图文并茂。github开源地址(欢迎star):https://github.com/itwanger/jmx-java如果有帮助请给二哥点个赞,这将是我继续分享的最强大动力!

猜你喜欢