感谢大神HashCodejava
感谢大神HashMapnode
@Test public void Integer_HashCode(){ Integer one = new Integer(20); System.out.println(one.hashCode()); //20 } /** * Integer 的 hashCode 就是它的value * * public int hashCode() { * return Integer.hashCode(value); * } */
@Test public void String_HashCode(){ String str1 ="123"; System.out.println(str1.hashCode()); // 48690 } /** * ASCII http://tool.oschina.net/commons?type=4 * String 类的散列值就是依次遍历其每一个字符成员, * 递归的将当前获得的hashCode乘以31而后加上下一个字符成员的ASCII值 (h = 31 * h + val[i];) * * h 初始为 0 * '1' 49 h = 31 * 0 + 49 = 49 * '2' 50 h = 31 * 49 + 50 = 1569 * '3' 51 h = 31 * 1569 + 51 = 48690 * * public int hashCode() { * int h = hash; * if (h == 0 && value.length > 0) { * char val[] = value; * * for (int i = 0; i < value.length; i++) { * h = 31 * h + val[i]; * } * hash = h; * } * return h; * } */
关于网上的一些解释git
Why does Java’s hashCode() in String use 31 as a multiplier?github
排名第一的答案web
The value 31 was chosen because it is an odd prime. If it were even and the multiplication overflowed, information would be lost, as multiplication by 2 is equivalent to shifting. The advantage of using a prime is less clear, but it is traditional. A nice property of 31 is that the multiplication can be replaced by a shift and a subtraction for better performance: 31 * i == (i << 5) - i`. Modern VMs do this sort of optimization automatically. 选择数字31是由于它是一个奇质数,,相对来讲,若是选择一个偶数会在乘法运算中产生溢出,致使数值信息丢失,由于乘二至关于移位运算。 选择质数的优点并非特别的明显,但这是一个传统。同时,数字31有一个很好的特性,即乘法运算能够被移位和减法运算取代,来获取更好的性能:31 * i == (i << 5) - i,现代的 Java 虚拟机能够自动的完成这个优化。 (h = 31 * h + val[i];)
排名第二的答案 ,后面有可视化验证算法
As Goodrich and Tamassia point out, If you take over 50,000 English words (formed as the union of the word lists provided in two variants of Unix), using the constants 31, 33, 37, 39, and 41 will produce less than 7 collisions in each case. Knowing this, it should come as no surprise that many Java implementations choose one of these constants. 正如 Goodrich 和 Tamassia 指出的那样,若是你对超过 50,000 个英文单词(由两个不一样版本的 Unix 字典合并而成)进行 hash code 运算,并使用常数 31, 33, 37, 39 和 41 做为乘子,每一个常数算出的哈希值冲突数都小于7个,因此在上面几个常数中,常数 31 被 Java 实现所选用也就不足为奇了。
31 * i = (i << 5) - i
。现代的 Java 虚拟机能够自动的完成这个优化假设 n=3 i=0 -> h = 31 * 0 + val[0] i=1 -> h = 31 * (31 * 0 + val[0]) + val[1] i=2 -> h = 31 * (31 * (31 * 0 + val[0]) + val[1]) + val[2] h = 31*31*31*0 + 31*31*val[0] + 31*val[1] + val[2] h = 31^(n-1)*val[0] + 31^(n-2)*val[1] + val[2] 这里先分析质数2。-------仅计算公式中次数最高的那一项----------- 首先,假设 n = 6,而后把质数2和 n 带入上面的计算公式。结果是2^5 = 32,是否是很小。因此这里能够判定,当字符串长度不是很长时,用质数2作为乘子算出的哈希值,数值不会很大。也就是说,哈希值会分布在一个较小的数值区间内,分布性不佳,最终可能会致使冲突率上升,质数2作为乘子会致使哈希值分布在一个较小区间内 那么若是用一个较大的大质数101会产生什么样的结果呢?根据上面的分析,我想你们应该能够猜出结果了。就是不用再担忧哈希值会分布在一个小的区间内了,由于101^5 = 10,510,100,501。可是要注意的是,这个计算结果太大了。若是用 int 类型表示哈希值,结果会溢出,最终致使数值信息丢失。若是不在乎质数101容易致使数据信息丢失问题,或许其是一个更好的选择。 尽管数值信息丢失并不必定会致使冲突率上升,可是咱们暂且先认为质数101(或者更大的质数)也不是很好的选择。最后,咱们再来看看质数31的计算结果: 31^5 = 28629151,结果值相对于32和10,510,100,501来讲。是否是很nice,不大不小。
计算哈希算法冲突率并不难,好比能够一次性将全部单词的 hash code 算出,并放入 Set 中去除重复值。以后拿单词数减去 set.size() 便可得出冲突数,有了冲突数,冲突率就能够算出来了。固然,若是使用 JDK8 提供的流式计算 API,则可更方便算出,代码片断以下:segmentfault
public static Integer hashCode(String str, Integer multiplier) { int hash = 0; for (int i = 0; i < str.length(); i++) { hash = multiplier * hash + str.charAt(i); } return hash; } /** * 计算 hash code 冲突率,顺便分析一下 hash code 最大值和最小值,并输出 * @param multiplier * @param hashs */ public static void calculateConflictRate(Integer multiplier, List<Integer> hashs) { Comparator<Integer> cp = (x, y) -> x > y ? 1 : (x < y ? -1 : 0); int maxHash = hashs.stream().max(cp).get(); int minHash = hashs.stream().min(cp).get(); // 计算冲突数及冲突率 int uniqueHashNum = (int) hashs.stream().distinct().count(); int conflictNum = hashs.size() - uniqueHashNum; double conflictRate = (conflictNum * 1.0) / hashs.size(); System.out.println(String.format("multiplier=%4d, minHash=%11d, maxHash=%10d, conflictNum=%6d, conflictRate=%.4f%%", multiplier, minHash, maxHash, conflictNum, conflictRate * 100)); }
从上图能够看出,数组
简单总结微信
一、 质数二、冲突率达到了 55%以上,并且hash值分布不是很普遍,,仅仅分布在了整个哈希空间的正半轴部分,即 0 ~ 2^31-1。而负半轴 -2^31 ~ -1,则无分布。这也证实了咱们上面断言,即质数2做为乘子时,对于短字符串,生成的哈希值分布性不佳。less
二、奇质数,10一、109 表现也不错,冲突率很低,说明了哈希值溢出不必定致使冲突率比较高,可是溢出的话,咱们不认为是咱们的优选乘子 ,若是不在乎质数101容易致使数据信息丢失问题,或许其是一个更好的选择。
三、偶数 3二、36 这两个并很差,32的冲突率已经超过了50%,尽管36表现好一些,可是和31,37相比,冲突率仍是比较高的,可是**偶数也不必定做为乘子冲突率就比较高 **
四、奇质数 3一、37 、41 表现都不出,冲突数都小于7个,使用较小的质数作为乘子时,冲突率会很高。 选择比较大的质数做为乘子时,冲突率会下降,可是可能会再次哈希值溢出
上面的2.2介绍了不一样数字做为乘子的冲突率状况,下面分析一下不一样数字做为乘子时,hash值得分布状况
http://www.javashuo.com/article/p-wpfvgnad-v.html
从Object角度看,JVM每new一个Object,它都会将这个Object丢到一个Hash表中去,这样的话,下次作Object的比较或者取这个对象的时候(读取过程),它会根据对象的HashCode再从Hash表中取这个对象。这样作的目的是提升取对象的效率。若HashCode相同再去调用equal。
(1)HashCode的存在主要是用于查找的快捷性,如Hashtable,HashMap等,HashCode是用来在散列存储结构中肯定对象的存储地址的;
(2)若是两个对象相同, equals方法必定返回true,而且这两个对象的HashCode必定相同;除非重写了方法
(3)若是对象的equals方法被重写,那么对象的HashCode也尽可能重写,而且产生HashCode使用的对象,必定要和equals方法中使用的一致,不然就会违反上面提到的第2点;
(4)两个对象的HashCode相同,并不必定表示两个对象就相同,也就是equals方法不必定返回true,只可以说明这两个对象在散列存储结构中,如Hashtable,他们存放在同一个篮子里。
Java中的集合(Collection)有两类,一类是List,再有一类是Set。前者集合内的元素是有序的,元素能够重复;后者元素无序,但元素不可重复。 equals方法可用于保证元素不重复,可是,若是每增长一个元素就检查一次,若是集合中如今已经有1000个元素,那么第1001个元素加入集合时,就要调用1000次equals方法。这显然会大大下降效率。
因而,Java采用了哈希表的原理。
哈希算法也称为散列算法,是将数据依特定算法直接指定到一个地址上。这样一来,当集合要添加新的元素时,先调用这个元素的HashCode方法,就一会儿能定位到它应该放置的物理位置上。
(1)若是这个位置上没有元素,它就能够直接存储在这个位置上,不用再进行任何比较了;
(2)若是这个位置上已经有元素了,就调用它的equals方法与新元素进行比较,相同的话就不存了;
(3)不相同的话,也就是发生了Hash key相同致使冲突的状况,那么就在这个Hash key的地方产生一个链表,将全部产生相同HashCode的对象放到这个单链表上去,串在一块儿(不多出现)。这样一来实际调用equals方法的次数就大大下降了,几乎只须要一两次。 (下面一、的实例就为这里的测试实例)
(1)例如内存中有这样的位置 :0 1 2 3 4 5 6 7
而我有个类,这个类有个字段叫ID,我要把这个类存放在以上8个位置之一,若是不用HashCode而任意存放,那么当查找时就须要到这八个位置里挨个去找,或者用二分法一类的算法。
但若是用HashCode那就会使效率提升不少。 定义咱们的HashCode为ID%8,好比咱们的ID为9,9除8的余数为1,那么咱们就把该类存在1这个位置,若是ID是13,求得的余数是5,那么咱们就把该类放在5这个位置。依此类推。
(2)可是若是两个类有相同的HashCode,例如9除以8和17除以8的余数都是1,也就是说,咱们先经过 HashCode来判断两个类是否存放某个桶里,但这个桶里可能有不少类,好比hashtable,那么咱们就须要再经过 equals 在这个桶里找到咱们要的类。
public class HashTest { private int i; public int getI() { return i; } public void setI(int i) { this.i = i; } @Override public int hashCode() { return i % 10; } /** * 对象的内存地址与hashcode有关系,但并非hashcode值相等,就是表明内存地址相同,这种想法是幼稚的 * 好比hashtable中hashcode值相等, * 可是存了不少的对象,这代表对象的== 确定不相等,Ojbect逆向推理,equals不相等,==确定不相等 * */ public final static void main(String[] args) { HashTest a = new HashTest(); HashTest b = new HashTest(); System.out.println(a.hashCode() == b.hashCode()); //true 人为制造hashcode值相同 System.out.println(a==b); //false //== 比较对象的相等比较对象引用地址是否相等。还要要比较对象内容是否相等 System.out.println(a.equals(b)); //false 不一样的对象 object中 == 和euqals是同样的 a.setI(1); b.setI(1); Set<HashTest> set = new HashSet<HashTest>(); set.add(a); set.add(b); //没有 equels 重写的状况 System.out.println(a.hashCode() == b.hashCode()); //true hashcode相同 System.out.println(a.equals(b)); //false 不一样的对象 ,建立出来的是地址就不一样了 //2 这个时候会发想存入了两个值 set中存放是根据hashcode值存放,若是hashcode值相同, //再比较equals值,若是equals值也相同,则产生一个单链表放进去 System.out.println(set.size()); }
public class HashTest { private int i; public int getI() { return i; } public void setI(int i) { this.i = i; } @Override public boolean equals(Object object) { if (object == null) { return false; } if (object == this) { return true; } if (!(object instanceof HashTest)) { return false; } HashTest other = (HashTest) object; if (other.getI() == this.getI()) { return true; } return false; } @Override public int hashCode() { return i % 10; } public final static void main(String[] args) { HashTest a = new HashTest(); HashTest b = new HashTest(); System.out.println(a.hashCode() == b.hashCode()); System.out.println(a==b); System.out.println(a.equals(b)); a.setI(1); b.setI(1); Set<HashTest> set = new HashSet<HashTest>(); set.add(a); set.add(b); System.out.println(a.hashCode() == b.hashCode()); System.out.println(a.equals(b)); System.out.println(set.size()); } } }
package com.hlj.util.Z009HashCode; import java.util.HashMap; import java.util.List; import java.util.Map; /** * @Description * @Author HealerJean * @Date 2018/4/12 */ public class D02_Object { public static void main(String[] args) { Map <Object,Object> map = new HashMap<>(); Object o = new Object(); map.put(o,"Stromg"); Object o2 = new Object(); map.put(o2,3); System.out.println(map.get(o)); //Stromg System.out.println(map.get(o2)); //3 o = 456; System.out.println(map.get(o)); //null 由于提供的key的hashcode的地址不是咱们存储的地址,因此不能找到,可是以前存储的还在 Person person = new Person(); person.setAge(1); map.put(person,3); System.out.println(map.get(person)); //3 person.setName("HealerJean"); System.out.println(map.get(person)); //3 System.out.println("map的大小"+map.size()); //map的大小3 person = null ; // person设置为null, map中仍是具备该person对象 System.out.println(map.get(person)); //null System.out.println("map的大小"+map.size()); //map的大小3 System.out.println("打印map的结果集"); Object[] list = map.keySet().toArray(); for (Object object :list){ System.out.println("key="+object.toString()+"||||| value="+map.get(object).toString()); } } } class Person { int age; String name; public int getAge() { return age; } public void setAge(int age) { this.age = age; } public String getName() { return name; } public void setName(String name) { this.name = name; } @Override public String toString() { return "Person{" + "age=" + age + ", name='" + name + '\'' + '}'; } } 打印结果 /** * Stromg * 3 * null * 3 * 3 * map的大小3 * null * map的大小3 * 打印map的结果集 * key=java.lang.Object@9807454||||| value=3 * key=Person{age=1, name='HealerJean'}||||| value=3 * key=java.lang.Object@4fca772d||||| value=Stromg */
上面明白了hashcode的生成原理了,如今咱们来看看 hash算法
hash值的做用,知道hash是为了获取数组下标的,很明显就知道该hash值是为了均匀散列表的下标,仔细看看,就知道下面使用了 hashcode 右移16位再和本身异或获得的hash值
static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }
观察下HashMap查找到数组的
// 重点关注 first = tab[(n - 1) & hash]) != null final Node<K,V> getNode(int hash, Object key) { // 该处的hash其实是调用的上面的hash方法 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; }
举例说明
对象 A 的 hashCode 为 0100 0010 0011 1000 1000 0011 1100 0000
对象 B 的 hashCode 为 0011 1011 1001 1100 0101 0000 1010 0000
若是不通过hash运算,若是数组长度是16(默认就是16),也就是 15 与运算这两个数,
15 的二进制数 0000 0000 0000 0000 0000 0000 0000 1111 ,发现结果都是0。这样的话数组小标就都是0了,这样的结果应该不是咱们想看到的,由于这种状况其实出现的次数挺多的。
解决
若是咱们将 hashCode 值右移 16 位,也就是取 int 类型的一半,恰好将该二进制数对半切开。而且使用位异或运算(若是两个数对应的位置相反,则结果为1,反之为0),这样的话,就能避免咱们上面的状况的发生,即(h = key.hashCode()) ^ (h >>> 16)。 至关于让本身的前半段16位和后半段16位作一个异或的运算
总的来讲,使用位移 16 位和 异或 就是防止这种极端状况。可是,该方法在一些极端状况下仍是有问题,好比:10000000000000000000000000 和 1000000000100000000000000 这两个数,若是数组长度是16,那么即便右移16位,在异或,hash 值仍是会重复。可是为了性能,对这种极端状况,JDK 的做者选择了性能。毕竟这是少数状况,为了这种状况去增长 hash 时间,性价比不高。
当数组吃长度n为 16 的时候数组下标: 1111 & 101010100101001001000(随便写的) 1000 = 8
其中 n 是数组的长度。其实该算法的结果和模运算的结果是相同的。可是,对于现代的处理器来讲,**除法和求余数(模运算)**是最慢的动做,
经过上面的4.1末尾,能够看到,能够看到,当 n 为 2 的幂次方的时候,减一以后就会获得 一堆1111…… 的数字,这个数字正好能够掩码 (都是一堆 1111……),而且获得的结果取决于 hash 值。由于 hash 位值是1,那么最终的结果也是1 ,hash位 值是0,最终的结果也是0。
tab[ (n - 1) & hash ];
接4.二、hash 算法的目的是为了让hash值均匀的分布在桶中(数组),那么,如何作到呢?试想一下,若是不使用 2 的幂次方做为数组的长度会怎么样?
假设咱们的数组长度是10,仍是上面的公式:
1001 & 101010100101001001000 结果:1000 = 8
1001 & 101000101101001001001 结果:1001 = 9
1001 & 101010101101101001010 结果: 1000 = 8
1001 & 101100100111001101100 结果: 1000 = 8
结论
因此说,咱们必定要保证 & 中的二进制位全为 1,才能最大限度的利用 hash 值,并更好的散列,只有全是1 ,才能有更多的散列结果。若是是 1001,有的散列结果是永远都不会出现的,好比 1111,1010,1011,…,只要 & 以前的数有 0, 对应的 1 确定就不会出现(由于只有都是1才会为1)。大大限制了散列的范围。
那咱们如何自定义呢?自从有了阿里的规约插件,每次楼主都要初始化容量,若是咱们预计咱们的散列表中有2个数据,那么我就初始化容量为2嘛
绝对不行,若是你们看过源码就会发现,若是Map中已有数据的容量达到了初始容量的 75%,那么散列表就会扩容,而扩容将会从新将全部的数据从新散列,性能损失严重,因此,咱们能够必需要大于咱们预计数据量的 1.34 倍,若是是2个数据的话,就须要初始化 2.68 个容量。固然这是开玩笑的,2.68 不能够,3 可不能够呢?确定也是不能够的,我前面说了,若是不是2的幂次方,散列结果将会大大降低。致使出现大量链表。那么我能够将初始化容量设置为4。 固然了,若是你预计大概会插入 12 条数据的话,那么初始容量为16简直是完美,一点不浪费,并且也不会扩容。
项目经验总计:
一、好比链表数不少,确定是数组初始化长度不对,这样的话会形成拥挤
二、若是某个map很大,注意,确定是事先没有定义好初始化长度
假设,某个Map存储了10000个数据,那么他会扩容到 20000,实际上,根本不用 20000,只须要 10000* 1.34= 13400 个,而后向上找到一个2 的幂次方,也就是 16384 初始容量足够。
哈,博主很乐意和各路好友交流,若是满意,请打赏博主任意金额,感兴趣的在微信转帐的时候,备注您的微信或者其余联系方式。添加博主微信哦。
请下方留言吧。可与博主自由讨论哦
微信 | 微信公众号 | 支付宝 |
---|---|---|
![]() |
![]() |
![]() |