在说hash(key)方法之前,下面来简单模拟下HashMap中的put()方法,来查看哈希冲突:
最简单的情况,在下面代码中,用一个Object[]数组充当链表数组,判断str实例对象的hashCode值并得到在数组中存放的下标,然后放入数组。
/*
说明:
1.有一些对象需要放入到一个数组中(为什么要把它们放入数组,可以探究为什么会产生哈希算法?)
2.问题来了,怎么确定该对象应该放到数组中哪个位置,不可能每个程序员都制定一套规则,让这个对象放到某个位置,没有统一的标准
3.哈希算法就可以将一个对象映射成一串二进制,这串二进制又可以转换成十进制整数
4.也就是某对象和它通过哈希算法产生的十进制整数是映射关系,对应的
5.可以通过十进制来研究该把放入到数组中哪个位置
6.发现用十进制数对数组长度取模,可以将所有对象都限制在数组长度范围内
7.这就解决了把这对象放到数组中哪个位置的问题
*/
public class Demo {
public static void main(String[] args) {
String str=new String("abc");
System.out.println(str);
// 获取str的hashCode值
int hashCode = str.hashCode();
System.out.println(hashCode);
// 将获得的所有对象存放到一个长度为20的数组中
Object[] arr=new Object[20];
int index = hashCode%arr.length;
System.out.println(index);
// 然后将该对象放入数组中对应下标的位置
arr[index]=str;
System.out.println(Arrays.toString(arr));
}
}
当加入很多个对象时,可能某个对象的hashCode值对数组长度取模得到的下标,在数组中已经存在某个对象了,这就产生了冲突
/*
说明:
1.有一些对象需要放入到一个数组中(为什么要把它们放入数组,可以探究为什么会产生哈希算法?)
2.问题来了,怎么确定该对象应该放到数组中哪个位置,不可能每个程序员都制定一套规则,让这个对象放到某个位置,没有统一的标准
3.哈希算法就可以将一个对象映射成一串二进制,这串二进制又可以转换成十进制整数
4.也就是某对象和它通过哈希算法产生的十进制整数是映射关系,对应的
5.可以通过十进制来研究该把放入到数组中哪个位置
6.发现用十进制数对数组长度取模,可以将所有对象都限制在数组长度范围内
7.这就解决了把这对象放到数组中哪个位置的问题
8.但又要添加一个对象到数组中,也通过hashCode值确定了该放在数组中哪个位置
9.但是,在该位置已经存在一个对象了,而对于这种多个对象共用一个地址的情形,就称之为冲突
10.有了冲突,自然是需要解决冲突,解决冲突的其中一种就是链地址法,也称为拉链法
*/
public class Demo {
public static void main(String[] args) {
String str=new String("abc");
System.out.println(str);
// 获取str的hashCode值
int hashCode = str.hashCode();
System.out.println(hashCode);
// 将获得的所有对象存放到一个长度为20的数组中
Object[] arr=new Object[20];
int index = hashCode%arr.length;
System.out.println(index);
// 然后将该对象放入数组中对应下标的位置
arr[index]=str;
System.out.println(Arrays.toString(arr));
System.out.println("---------------------------------------------");
// 现在又要往数组中添加一个对象
String str2=new String("r");
System.out.println(str2);
int hashCode2 = str2.hashCode();
System.out.println(hashCode2);
int index2=hashCode2%20;
System.out.println(index2);
arr[index]=str2;// 发现"abc"和"r"在数组中存储的地址是一样的,这产生了冲突
System.out.println(Arrays.toString(arr));
}
}
事实上,我们并不希望后面的对象将已经存在的覆盖掉,就必须解决冲突,通过将元素存储在LinkedList中来模拟存储冲突元素的链表。
public class Demo2 {
public static void main(String[] args) {
String str=new String("abc");
System.out.println(str);
// 获取str的hashCode值
int hashCode = str.hashCode();
System.out.println(hashCode);
// 将获得的所有对象存放到一个长度为20的数组中
Object[] arr=new Object[20];
int index = hashCode%arr.length;
System.out.println(index);
// 然后将该对象放入数组中对应下标的位置
LinkedList<String> linkedList=new LinkedList<>();
linkedList.add(str);
arr[index]=linkedList;
// arr[index]=str;
System.out.println(Arrays.toString(arr));
System.out.println("---------------------------------------------");
// 现在又要往数组中添加一个对象
String str2=new String("r");
System.out.println(str2);
int hashCode2 = str2.hashCode();
System.out.println(hashCode2);
int index2=hashCode2%20;
System.out.println(index2);
if(arr[index2]!=null){// 不为null的话表示已经存在元素了,采用链地址法,将地址相同的对象挂在后面
LinkedList<String> list = (LinkedList)arr[index2];
list.add(str2);
}
// arr[index]=str2;// 发现"abc"和"r"在数组中存储的地址是一样的,这产生了冲突
System.out.println(Arrays.toString(arr));
}
}
下面是来模拟26个英文字母存放到哈希表中:
public class Demo3 {
public static void main(String[] args) {
// 26个英文字母的字符串
String va = "abcdefghijklmnopqrstuvwxyz";
// 将字符串转成字符数组
char[] vaArr = va.toCharArray();
// 创建一个链表数组,用来存储对象
Object[] obj = new Object[10];
// 循环遍历字符数组
for (Character c : vaArr) {
// 获取到该字符对象的hashCode值,是一个十进制的整数
int hashCode = c.hashCode();
// 将hashCode值对数组长度取模,得到该字符对象应该存放到数组的哪一个位置
int index = hashCode % obj.length;
// 判断得到的下标在数组中是否已经有元素,没有元素则为null
if (obj[index] == null) {
// 创建一个链表,存放冲突的元素
LinkedList<Object> linkedList = new LinkedList<>();
// 添加元素到链表中
linkedList.add(c);
// 将指定下标处赋值
obj[index] = linkedList;
} else {
// 如果已经存在元素了,就发生了冲突,解决冲突的办法就是将结点挂在已有的元素后面,这就是拉链法解决
// 获取已经存在的链表
LinkedList<Object> linkedList = (LinkedList<Object>) obj[index];
// 将元素添加进去
linkedList.add(c);
}
}
// 打印数组
System.out.println(Arrays.toString(obj));
}
}
看到打印结果真的是冲突的元素非常多,将数组长度调整为20,情况有所好转
调为50,发现冲突的元素就少了很多,几乎没有冲突。
上面这些只是对哈希表有个更加深刻的认识,下面来谈谈hash(key)方法。
通过上面的代码就知道了哈希表的原理很简单,调用Object对象的hashCode()方法,该方法会返回一个整数,然后用这个数对HashMap或者HashTable的容量进行取模就行了。
但HashMap的实现却不是这样简单,更多的需要为效率考虑。
所以hash(key)方法就是返回哈希值,查看源码如下:
final int hash(Object k) {
int h = hashSeed;
if (0 != h && k instanceof String) {
return sun.misc.Hashing.stringHash32((String) k);
}
h ^= k.hashCode();
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
那么思考下,为什么不直接如下面这样呢?
int hash (Object k) {
return k.hashCode();
}
多么简洁易读,而且我们上面的模拟就是这样干的,那为什么不这样呢?
看下面的代码:
public class Demo2 {
public static void main(String[] args) {
String str=new String("abc");
System.out.println(str);
// 获取str的hashCode值
int hashCode = str.hashCode();
System.out.println(hashCode);
// 将获得的所有对象存放到一个长度为16的数组中,满足使用位运算的情况,length是2的n次方
Object[] arr=new Object[16];
int index = hashCode%arr.length;
System.out.println(index);
// 然后将该对象放入数组中对应下标的位置
LinkedList<String> linkedList=new LinkedList<>();
linkedList.add(str);
arr[index]=linkedList;
System.out.println(Arrays.toString(arr));
System.out.println("---------------------------------------------");
// 现在又要往数组中添加一个对象
String str2=new String("r");
System.out.println(str2);
int hashCode2 = str2.hashCode();
System.out.println(hashCode2);
int index2=hashCode2%arr.length;
System.out.println(index2);
if(arr[index2]!=null){// 不为null的话表示已经存在元素了,采用链地址法,将地址相同的对象挂在后面
LinkedList<String> list = (LinkedList)arr[index2];
list.add(str2);
}else {
LinkedList<String> list=new LinkedList<>();
list.add(str2);
arr[index2]=list;
}
System.out.println(Arrays.toString(arr));
}
}
发现index最后得出都是2,发生了冲突
使用了扰动算法,代码如下:
public class Demo2 {
public static void main(String[] args) {
String str = new String("abc");
System.out.println(str);
// 获取str的hashCode值
int hashCode = str.hashCode();
System.out.println(hashCode);
// 将获得的所有对象存放到一个长度为16的数组中,满足使用位运算的情况,length是2的n次方
Object[] arr = new Object[16];
int index = hash(str) % arr.length;
System.out.println(index);
// 然后将该对象放入数组中对应下标的位置
LinkedList<String> linkedList = new LinkedList<>();
linkedList.add(str);
arr[index] = linkedList;
System.out.println(Arrays.toString(arr));
System.out.println("---------------------------------------------");
// 现在又要往数组中添加一个对象
String str2 = new String("r");
System.out.println(str2);
int hashCode2 = str2.hashCode();
System.out.println(hashCode2);
int index2 = hash(str2) % arr.length;
System.out.println(index2);
if (arr[index2] != null) {// 不为null的话表示已经存在元素了,采用链地址法,将地址相同的对象挂在后面
LinkedList<String> list = (LinkedList) arr[index2];
list.add(str2);
} else {
LinkedList<String> list = new LinkedList<>();
list.add(str2);
arr[index2] = list;
}
System.out.println(Arrays.toString(arr));
}
/**
* 返回使用了扰动算法的hashCode
*
* @param k 要计算hashCode值的对象
* @return 返回使用了扰动算法的hashCode
*/
static int hash(Object k) {
int h = 0;
h ^= k.hashCode();
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
}
其中hash()方法内的代码就是JDK中hash()方法的源码,对hashCode值进行扰动计算。
发现原来冲突的两个对象,现在不冲突了,下标index变成了2和5。
下面来看看扰动算法的每一步结果:
"abc" | "r" | |
---|---|---|
k.hashCode()得到的十进制值 | 96354 | 114 |
k.hashCode()转换的二进制值 | 0000 0000 0000 0001 0111 1000 0110 0010 | 0000 0000 0000 0000 0000 0000 0111 0010 |
h=0 | 0000 0000 0000 0000 0000 0000 0000 0000 | 0000 0000 0000 0000 0000 0000 0000 0000 |
h^=k.hashCode() | 0000 0000 0000 0001 0111 1000 0110 0010 | 0000 0000 0000 0000 0000 0000 0111 0010 |
h>>>20 | 0000 0000 0000 0000 0000 0000 0000 0000 | 0000 0000 0000 0000 0000 0000 0000 0000 |
h>>>12 | 0000 0000 0000 0000 0000 0000 0001 0111 | 0000 0000 0000 0000 0000 0000 0000 0000 |
h ^= (h >>> 20) ^ (h >>> 12) | 0000 0000 0000 0001 0111 1000 0111 0101 | 0000 0000 0000 0000 0000 0000 0111 0010 |
h >>> 7 | 0000 0000 0000 0000 0000 0010 1111 0000 | 0000 0000 0000 0000 0000 0000 0000 0000 |
h >>> 4 | 0000 0000 0000 0000 0001 0111 1000 0111 | 0000 0000 0000 0000 0000 0000 0000 0111 |
return h ^ (h >>> 7) ^ (h >>> 4) | 0000 0000 0000 0001 0110 1101 0000 0010 | 0000 0000 0000 0000 0000 0000 0111 0101 |
h ^ (h >>> 7) ^ (h >>> 4) % 16 | 0000 0000 0000 0000 0000 0000 0000 0010 | 0000 0000 0000 0000 0000 0000 0000 0101 |
转换成十进制 | 2 | 5 |
所以hash(key)扰动函数是对key的hashCode进行扰动运算,让高位也参与运算,让分布更加散列,最终达到降低哈希冲突的目的。
注意:这是JDK1.7中的hash方法,而1.8变化较大。
总结:
- hash(key)方法是为了获取到键(key)的hashCode,并使用扰动算法,得到更加散列的值,降低发生哈希冲突的概率。
- indexFor(hash, length)方法是为了获取该对象应该存储在链表数组中哪个下标位置,使用位运算(&)能够提高效率。
参考博客:
全网把Map中的hash()分析的最透彻的文章,别无二家。
Java高级之1.7版本JDK中的HashMap的indexFor()方法