目录
为什么会有hashCode 先抛一个结论
hashCode的设计初衷是提高哈希容器的性能
抛开hashCode,现在让你对比两个对象是否相等,你会怎么做?
thisObj == thatObj thisObj.equals(thatObj)
我想不出第三种了,而且这两种其实没啥大的区别,object的equals()方法底层也是==,jdk1.8 Object类的第148行;
public boolean equals(Object obj) { return (this == obj); }
为什么有了equals还要有hashCode,既生瑜何生亮??上面说了,hashCode的设计初衷是提高哈希容器的性能,equals的效率是比hashCode高的,不信的可以自己去试一下;
像我们常用的HashMap、HashTable等,理论上讲是可以不要hashCode的,但是会牺牲很多性能,这肯定不是我们想看到的,互联网时代,比的就是谁更快;
所以为什么说java离不开hashCode,答案就是为了提高hash容器性能;
什么是hashCode 知道hashCode存在的意义后,我们来研究下hashCode,看下长什么样
对象调用hashCode方法后,会返回一串int类型的数字码
BloomFilterTest bloomFilter = new BloomFilterTest(); log.info("对象的hashcode:{}", bloomFilter.hashCode()); log.info("1433223的hashcode:{}", "1433223".hashCode()); log.info("郭德纲的hashcode:{}", "郭德纲".hashCode()); log.info("小郭德纲的hashcode:{}", "小郭德纲".hashCode()); log.info("彭于晏的hashcode:{}", "彭于晏".hashCode()); log.info("唱跳rap篮球的hashcode:{}", "唱跳rap篮球".hashCode());
运行结果
对象的hashcode:459296537 1433223的hashcode:2075391824 郭德纲的hashcode:36446088 小郭德纲的hashcode:738530585 彭于晏的hashcode:24125870 唱跳rap篮球的hashcode:-767899628 ##因为返回值是int类型,有负数很正常
可以看出,对象的hashcode值跟对象本身的值没啥联系,比如郭德纲和小郭德纲,虽然只差一个字,它们的hashCode值不能说一模一样,也可以说是毫不相干,嘤嘤嘤~
hashCode和equals的关系
java规定:
如果两个对象的hashCode()相等,那么他们的equals()不一定相等。 如果两个对象的equals()相等,那么他们的hashCode()必定相等。
还有一点,重写equals()方法时候一定要重写hashCode()方法,不要问为什么,无脑写就行了,会省很多事
前面都是铺垫,这才是今天的主题
我们以HashMap的hash算法来看,个人认为这是很值得搞懂的hash算法,设计超级超级巧妙
static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }
这是hashMap的hash算法,我们一步一步来看
(h = key.hashCode()) ^ (h >>> 16)
hashCode就hashCode嘛,为啥还要>>>16,这个 ^ 又是啥,不着急一个一个来说
hashMap我们知道默认初始容量是16,也就是有16个桶,那hashmap是通过什么来计算出put对象的时候该放到哪个桶呢
final Node<K,V> getNode(int hash, Object key) { Node<K,V>[] tab; Node<K,V> first, e; int n; K k; if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) { if (first.hash == hash && // always check first node ((k = first.key) == key || (key != null && key.equals(k)))) return first; if ((e = first.next) != null) { if (first instanceof TreeNode) return ((TreeNode<K,V>)first).getTreeNode(hash, key); do { if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) return e; } while ((e = e.next) != null); } } return null; }
上面是hashmap的getNode方法,对hashmap源码有兴趣的同学自行研究,我们今天主要看这一句: (n - 1) & hash
也就是说hashmap是通过数组长度-1&key的hash值来计算出数组下标的,这里的hash值就是上面(h = key.hashCode()) ^ (h
16)计算出来的值
不要慌不要慌不要慌,看不懂没关系,我们现在总结下目前的疑问
为什么数组长度要 - 1,直接数组长度&key.hashCode不行吗 为什么要length-1 & key.hashCode计算下标,而不是用key.hashCode % length 为什么要^运算 为什么要>>>16
先说结论
数组长度-1、^运算、>>>16,这三个操作都是为了让key在hashmap的桶中尽可能分散 用&而不用%是为了提高计算性能
我们先看下如果数组长度不-1和不进行>>>16运算造成的结果,知道了结果我们后面才来说为什么,这样子更好理解
log.info("数组长度不-1:{}", 16 & "郭德纲".hashCode()); log.info("数组长度不-1:{}", 16 & "彭于晏".hashCode()); log.info("数组长度不-1:{}", 16 & "李小龙".hashCode()); log.info("数组长度不-1:{}", 16 & "蔡徐鸡".hashCode()); log.info("数组长度不-1:{}", 16 & "唱跳rap篮球鸡叫".hashCode()); log.info("数组长度-1但是不进行异或和>>>16运算:{}", 15 & "郭德纲".hashCode()); log.info("数组长度-1但是不进行异或和>>>16运算:{}", 15 & "彭于晏".hashCode()); log.info("数组长度-1但是不进行异或和>>>16运算:{}", 15 & "李小龙".hashCode()); log.info("数组长度-1但是不进行异或和>>>16运算:{}", 15 & "蔡徐鸡".hashCode()); log.info("数组长度-1但是不进行异或和>>>16运算:{}", 15 & "唱跳rap篮球鸡叫".hashCode()); log.info("数组长度-1并且进行异或和>>>16运算:{}", 15 & ("郭德纲".hashCode()^("郭德纲".hashCode()>>>16))); log.info("数组长度-1并且进行异或和>>>16运算:{}", 15 & ("彭于晏".hashCode()^("彭于晏".hashCode()>>>16))); log.info("数组长度-1并且进行异或和>>>16运算:{}", 15 & ("李小龙".hashCode()^("李小龙".hashCode()>>>16))); log.info("数组长度-1并且进行异或和>>>16运算:{}", 15 & ("蔡徐鸡".hashCode()^("蔡徐鸡".hashCode()>>>16))); log.info("数组长度-1并且进行异或和>>>16运算:{}", 15 & ("唱跳rap篮球鸡叫".hashCode()^("唱跳rap篮球鸡叫".hashCode()>>>16)));
数组长度不-1:0 数组长度不-1:0 数组长度不-1:16 数组长度不-1:16 数组长度不-1:16 数组长度-1但是不进行异或和>>>16运算:8 数组长度-1但是不进行异或和>>>16运算:14 数组长度-1但是不进行异或和>>>16运算:8 数组长度-1但是不进行异或和>>>16运算:2 数组长度-1但是不进行异或和>>>16运算:14 数组长度-1并且进行异或和>>>16运算:4 数组长度-1并且进行异或和>>>16运算:14 数组长度-1并且进行异或和>>>16运算:7 数组长度-1并且进行异或和>>>16运算:13 数组长度-1并且进行异或和>>>16运算:2
一下就看出区别了哇,第一组返回的下标就只有0和16,第二组也只有2、8、14,第三组的下标就很分散,这才是我们想要的
这结合hashMap来看,前两组造成的影响就是key几乎全部怼到同一个桶里,及其不分散,用行话讲就是有太多的hash冲突,这对hashMap的性能有很大影响,hash冲突造成的链表红黑树转换那些具体的原因这里就不展开说了
原理 知道了结果,现在说说其中的玄学
1、为什么数组长度要 - 1,直接数组长度&key.hashCode不行吗
hashMap默认长度是16,看看16的二进制码是多少
log.info("16的二进制码:{}",Integer.toBinaryString(16)); //16的二进制码:10000,
再看看key.hashCode()的二进制码是多少,以郭德纲为例
log.info("key的二进制码:{}",Integer.toBinaryString("郭德纲".hashCode())); //key的二进制码:10001011000001111110001000 length & key.hashCode() => 10000 & 10001011000001111110001000 位数不够,高位补0,即 0000 0000 0000 0000 0000 0001 0000 & 0010 0010 1100 0001 1111 1000 1000 &运算规则是第一个操作数的的第n位于第二个操作数的第n位都为1才为1,否则为0 所以结果为0000 0000 0000 0000 0000 0000 0000,即 0
冷静分析,问题就出在16的二进制码上,它码是10000,只有遇到hash值二进制码倒数第五位为1的key他们&运算的结果才不等于0,这句话好好理解下,看不懂就别强制看,去摸会儿鱼再回来看
再来看16-1的二进制码,它码是1111,同样用郭德纲这个key来举例
(length-1) & key.hashCode() => 1111 & 10001011000001111110001000 位数不够,高位补0,即 0000 0000 0000 0000 0000 0000 1111 & 0010 0010 1100 0001 1111 1000 1000 &运算规则是第一个操作数的的第n位于第二个操作数的第n位都为1才为1,否则为0 所以结果为0000 0000 0000 0000 0000 0000 1000,即 8
如果还看不出这其中的玄机,你就多搞几个key来试试,总之记住,限制它们&运算的结果就会有很多种可能性了,不再受到hash值二进制码倒数第五位为1才能为1的限制
2、为什么要length-1&key.hashCode计算下标,而不是用key.hashCode%length
这个其实衍生出三个知识点
1、其实(length-1)&key.hashCode计算出来的值和key.hashCode%length是一样的
log.info("(length-1)&key.hashCode:{}",15&"郭德纲".hashCode()); log.info("key.hashCode%length:{}","郭德纲".hashCode()%16); // (length-1)&key.hashCode:8 // key.hashCode%length:8
那你可能更蒙逼了,都一样的为啥不用%,这就要说到第二个知识点
2、只有当length为2的n次方时,(length-1)&key.hashCode才等于key.hashCode%length,比如当length为15时
log.info("(length-1)&key的hash值:{}",14&"郭德纲".hashCode()); log.info("key的hash值%length:{}","郭德纲".hashCode()%15); // (length-1)&key.hashCode:8 // key.hashCode%length:3
可能又有小朋友会思考,我不管那我就想用%运算,要用魔法打败魔法,请看第三点
3、用&而不用%是为了提高计算性能,对于处理器来讲,&运算的效率是高于%运算的,就这么简单,除此之外,除法的效率也没&高
3、为什么要进行^运算
这是异或运算符,第一个操作数的的第n位于第二个操作数的第n位相反才为1,否则为0 我们多算几个key的值出来对比
//不进行异或运算返回的数组下标 log.info("郭德纲:{}", Integer.toBinaryString("郭德纲".hashCode())); log.info("彭于晏:{}", Integer.toBinaryString("彭于晏".hashCode())); log.info("李小龙:{}", Integer.toBinaryString("李小龙".hashCode())); log.info("蔡徐鸡:{}", Integer.toBinaryString("蔡徐鸡".hashCode())); log.info("唱跳rap篮球鸡叫:{}", Integer.toBinaryString("唱跳rap篮球鸡叫".hashCode())); 00001000101100000111111000 1000 00000101110000001000011010 1110 00000110001111100100010011 1000 00000111111111111100010111 0010 10111010111100100011001111 1110 进行&运算,看下它们返回的数组下标,length为16的话,只看后四位即可 8 14 8 2 14 //进行异或运算返回的数组下标 log.info("郭德纲:{}", Integer.toBinaryString("郭德纲".hashCode()^("郭德纲".hashCode()>>>16))); log.info("彭于晏:{}", Integer.toBinaryString("彭于晏".hashCode()^("彭于晏".hashCode()>>>16))); log.info("李小龙:{}", Integer.toBinaryString("李小龙".hashCode()^("李小龙".hashCode()>>>16))); log.info("蔡徐鸡:{}", Integer.toBinaryString("蔡徐鸡".hashCode()^("蔡徐鸡".hashCode()>>>16))); log.info("唱跳rap篮球鸡叫:{}", Integer.toBinaryString("唱跳rap篮球鸡叫".hashCode()^("唱跳rap篮球鸡叫".hashCode()>>>16))); 0000001000101100000111011010 0100 0000000101110000001000001101 1110 0000000110001111100100001011 0111 0000000111111111111100001000 1101 0010111010111100101000100100 0010 进行&运算,看下它们返回的数组下标,length为16的话,只看后四位即可 4 14 7 13 2
很明显,做了^运算的数组下标更分散
4、为什么要>>>16
这是无符号右移16位,位数不够,高位补0 其实>>>16和^运算是相辅相成的关系,目的都是为了让数组下标更分散,看一看>>>16的效果就知道了 (单独看<<<16可能看不出啥效果,但搭配上^就很容易看出来了,总之,记住这是为了让数组下标更加分散的骚操作)
log.info("郭德纲:{}", Integer.toBinaryString("郭德纲".hashCode())); log.info("郭德纲>>>16:{}", Integer.toBinaryString("郭德纲".hashCode()>>>16)); 0000 0010 0010 1100 0001 1111 1000 1000 0000 0000 0000 0000 0000 0010 0010 1100 log.info("彭于晏:{}", Integer.toBinaryString("彭于晏".hashCode())); log.info("彭于晏>>>16:{}", Integer.toBinaryString("彭于晏".hashCode()>>>16)); 0000 0001 0111 0000 0010 0001 1010 1110 0000 0000 0000 0000 0000 0001 0111 0000 log.info("李小龙:{}", Integer.toBinaryString("李小龙".hashCode())); log.info("李小龙>>>16:{}", Integer.toBinaryString("李小龙".hashCode()>>>16)); 0000 0001 1000 1111 1001 0001 0011 1000 0000 0000 0000 0000 0000 0001 1000 1111 log.info("蔡徐鸡:{}", Integer.toBinaryString("蔡徐鸡".hashCode())); log.info("蔡徐鸡>>>16:{}", Integer.toBinaryString("蔡徐鸡".hashCode()>>>16)); 0000 0001 1111 1111 1111 0001 0111 0010 0000 0000 0000 0000 0000 0001 1111 1111 log.info("唱跳rap篮球鸡叫:{}", Integer.toBinaryString("唱跳rap篮球鸡叫".hashCode())); log.info("唱跳rap篮球鸡叫>>>16:{}", Integer.toBinaryString("唱跳rap篮球鸡叫".hashCode()>>>16)); 0010 1110 1011 1100 1000 1100 1111 1110 0000 0000 0000 0000 0010 1110 1011 1100
原文链接:https://blog.csdn.net/qq_33709582/article/details/113337405?utm_medium=distribute.pc_category.none-task-blog-hot-13.nonecase&depth_1-utm_source=distribute.pc_category.none-task-blog-hot-13.nonecase&request_id=