Java高级之1.7版本JDK中的HashMap的indexFor()方法

注意:在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)算法生成的十进制整数对数组取模,得到对应的下标,确定该对象应该放到数组中的哪个位置,而使用位运算(&)是为了提高效率

猜你喜欢

转载自blog.csdn.net/cnds123321/article/details/113742176
今日推荐