注意:在1.8之前(本例是1.7版本)中才有indexFor()方法,而1.7及以后该方法没有了,该方法所产生的作用不再是单独作为一个方法出现。
该方法的源码:
static int indexFor(int h, int length) {
return h & (length-1);
}
知道这个方法肯定明白indexFor()方法将hash生成的整型转换成链表数组的下标。
而h&(length-1)的意思就是取模,即h%length,有下面的等式,但满足这个等式需要条件的。
h % length == h&(length-1)
// 该等式满足的条件是length必须是2的n次方
但代码为什么是这样呢?因为位运算(&)比取模运算(%)效率高得多,毕竟是直接操控二进制位,而取模运算还要转换成十进制。
那为什么上面的等式相等呢?看下面的代码,去运行下
public class Demo {
public static void main(String[] args) {
System.out.println(6 % 4);
System.out.println(6 & (4 - 1));
System.out.println();
System.out.println(9 % 16);
System.out.println(9 & (16 - 1));
System.out.println();
System.out.println(6%3);
System.out.println(6&(3-1));
}
}
/**
* 打印结果:
* 2
* 2
*
* 9
* 9
*
* 0
* 2
*/
发现如果length是2的n次方,则满足上面的等式,即可以使用&代替%,但如果length不是2的n次方,则该等式不成立。
为什么呢?&是属于位运算中的运算,而与(&)运算的运算规则如下:
0&0=0;0&1=0;1&0=0;1&1=1
即:两个同时为1,结果为1,否则为0
所以把上面的数转换成二进制来进行&运算,可以一目了然。
十进制6的二进制为:0000 0110
十进制4的二进制为:0000 0100
十进制3的二进制为:0000 0011
而6%4,即是
几个2的n次方十进制数转换成二进制得到的结果如下:
2的n次方所转换成二进制的结果如下:
2^0 0000 0001
2^1 0000 0010
2^2 0000 0100
2^3 0000 1000
2^4 0001 0000
2^5 0010 0000
2^6 0100 0000
2^7 1000 0000
所以上面这些是为了证明h % length == h&(length-1)这个等式是成立的,但如果length不是2的n次方则是不成立的,上面的代码已经证明了结果。
所以只需要满足length的长度是2的n次方,则可以使用位运算符来提高效率,来实现取模运算。
而indexFor是在HashMap中使用的,而HashMap中保证了length一定是2的n次方倍。
其中HashMap中的初始值是16,是2的4次方,而每次扩容扩为了原来容量的2倍,也就是32,即2的5次方,以后每次扩容,都保证了是2的n次方。
下面提供源码证明:
下面看下取模运算和位运算的执行效率差别
public class Demo {
public static void main(String[] args) {
int num=1000000000;// 10亿
mod(num);
bit(num);
}
private static void mod(int num){
// 取模运算的时间
long start=System.currentTimeMillis();
int result;
for (int i = 1; i <= num; i++) {
result=i%5;
}
long end=System.currentTimeMillis();
System.out.println("取模运算的时间:"+(end-start)+"毫秒");
}
private static void bit(int num){
// 位运算的时间
long start=System.currentTimeMillis();
int result;
for (int i = 1; i <= num; i++) {
result=i&5;
}
long end=System.currentTimeMillis();
System.out.println("位运算的时间:"+(end-start)+"毫秒");
}
}
打印的结果:
如果数据量大了之后,使用位运算效率比取模运算高得多。
一个简单但值得注意的问题:为什么对length取模就可以得到该对象存储在链表数组中的哪个下标呢?
要明白这个问题,需要对哈希表的实现结构一个认知,这里是采用的链地址法解决冲突,所以数据结构是采用的链表数组,即将哈希表的每个单元作为链表的头结点,所有哈希地址为i的元素构成一个同义词链表。即发生冲突时就把该关键字链在以该单元为头结点的链表的尾部。
将任意长度的二进制值串映射为固定长度的二进制值串,这个映射的规则就是哈希算法
,而通过原始数据映射之后得到的二进制值串就是哈希值(散列值)。
而hash(key)就是一个hash函数,生成一个十进制整数值,然后还要将这个整数值映射到指定大小的数组中,而要把超过数组长度的数映射到数组中,对数组长度取模即可,比如说数组长度length是13,而要把30放入数组中,那么用30%13取模运算后得到4,即放入到数组中索引为4的位置。
而在HashMap中这个数组的长度也是固定的,即length。
可能我还是没有说清楚,反而越来越糊涂了。
看下面的代码:
/*
说明:
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));
}
}
控制台打印结果为:
上面就简单地模拟了下,应该有更直观的感受了。
总结:indexFor()方法就是将hash(key)算法生成的十进制整数对数组取模,得到对应的下标,确定该对象应该放到数组中的哪个位置,而使用位运算(&)是为了提高效率。