当前位置: 首页 > 科技观察

布布HashCode,毕竟七嘴八舌

时间:2023-03-16 13:26:10 科技观察

大家好,我是沉默王二。假期结束了,要迅速切换到工作状态,投入到新的一天中。假期快乐玩耍,工作时间积极工作。这应该是我们大多数“现代人”应该有的生活状态。之所以尽量铺垫上一段,是想告诉大家,技术文章虽然晚了,加油,一起学习吧~今天我们来说说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指令。你有没有想过这样一个问题:为什么Object类需要一个hashCode()方法?在Java中,hashCode()方法的主要作用是配合哈希表。哈希表(HashTable),也叫哈希表,是一种可以通过key-value直接访问的数据结构。它最大的特点是可以快速实现查找、插入和删除。使用的算法称为哈希,将任意长度的输入转换为固定长度的输出,输出就是哈希值。使用像MD5和SHA1这样的哈希算法。和HashSet一样,Java中的Hashtable(注意小写的t)和HashMap都是基于哈希表的具体实现。其中,HashMap是最典型的代表。不仅面试官经常问,工作中使用频率也很高。想一想,如果没有哈希表,但是你需要这样一个数据结构,里面存储的数据不允许重复,怎么办?还是用equals()方法一个一个比较?这个方案当然是可行的。但是如果数据量特别大,使用equals()方法进行一对一比较的效率肯定很低。最好的解决方案是哈希表。以HashMap为例。当我们要向其中添加一个对象时,首先调用对象的hashCode()方法获取对应的hash值,然后将hash值和对象放入HashMap中。当我们要添加一个新的对象时:获取对象的哈希值;与已有的hash值进行比较,不相等则直接存储;如果存在相等,则调用equals()方法保存对象之间的比较,如果相等则不保存;如果不相等,说明hash冲突,增加一个链表存放新的对象;如果链表长度大于8,则转为红黑树处理。这样调用equals()方法的频率就大大降低了。换句话说,只要哈希算法足够高效,能够将哈希冲突的频率降到最低,哈希表的效率就特别高。我们看一下HashMap的hash算法:staticfinalinthash(Objectkey){inth;return(key==null)?0:(h=key.hashCode())^(h>>>16);}首先调用object()方法的hashCode,然后右移值再异或。一般来说,String会作为HashMap的key进行hash运算,那么我们看一下String的hashCode()方法:publicinthashCode(){inth=hash;if(h==0&&value.length>0){hash=h=isLatin1()?StringLatin1.hashCode(value):StringUTF16.hashCode(value);}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;privateStringname;publicStudent(intage,Stringname){this.age=age;this.name=name;}@Overridepublicbooleanequals(Objecto){Studentstudent=(Student)o;returnage==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)return0;intresult=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++源码。staticinlineintptr_tget_next_hash(Thread*current,oopobj){intptr_tvalue=0;if(hashCode==0){//ThisformusesglobalPark-MillerRNG.//OnMPsystemwe'llhavelotsofRWaccesstoaglobal,sothe//mechanismminduceslotsofcoherencytraffic.value=os::random();}elseif(hashCode==1){//Thisvariationhasthepropertyofbeingstable(idempotent)//betweenSTWoperations.Thiscanbeusefulinsomeofthe1-0//synchronizationschemes.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'sxor-shiftschemewiththread-specificstate//Thisisprobablythebestoverallimplementation--we'll//likelymakethisthedefaultinfuturereleases.unsignedt=current->_hashStateX;t^=(t<<11);current->_hashStateX=current复制代码->_hashStateY;current->_hashStateY=current->_hashStateZ;current->_hashStateZ=current->_hashStateW;unsignedv=current->_hashStateW;v=(v^(v>>19)^(t^(t>>8));current->_hashStateW=v;值=v;}value&=markWord::hash_mask;if(value==0)value=0xBAD;assert(value!=markWord::no_hash,"invariant");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值,不是线程安全的,多线程eads可能会得到相同的散列值。hashCode==4,与创建对象的内存位置有关,原样输出。hashCode==5,默认值,支持多线程,使用Marsaglia的xor-shift算法生成伪随机数。所谓xor-shift算法,简单来说,看起来就像一个移位寄存器,每次移入的位都是通过寄存器中的几个位进行异或运算产生的。所谓伪随机数并不是完全随机的,但很难产生真随机数,所以只要能通过一定的随机数统计检验,就可以作为真随机数使用。至于更深层次的挖掘,涉及到数学知识和物理知识,就不展开了。毕竟,食物是原罪。参考资料:https://zhuanlan.zhihu.com/p/121555725https://www.cnblogs.com/dolphin0520/p/3681042.html