JDK1.7 HashMap 源码分析(2)

<< : 按二进制形式把所有的数字向左移动对应的位数,高位移出(舍弃),低位的空位补零。在数字没有溢出的前提下,对于正数和负数,左移一位都相当于乘以2的1次方,左移n位就相当于乘以2的n次方;

>>: 按二进制形式把所有的数字向右移动对应位移位数,低位移出(舍弃),高位的空位补符号位,即正数补零,负数补1。右移一位相当于除2,右移n位相当于除以2的n次方。

>>>: 无符号右移,忽略符号位,空位都以0补齐

我们拿数字10做示例,经过(number - 1) << 1 = 18,二进制表示为:10010

i |= (i >>  1) 即:10010 | 01001 = 11011

i |= (i >>  2) 即:11011 | 00110 = 11111

i |= (i >>  4) 即:11111 | 00001 = 11111

……

其实这几步就是把i的最高位1之后的所有位都变成1

然后 i – (i >>> 1) 即:11111-01111=10000(16)

这步是把最高位,之后的都变成0,这样就求出了最接近10的2的N次方(16)

至于为什么要不数组的Size设置为2的N次方,我们后面说。

hash — 计算Key的hash值

01

02

03

04

05

06

07

08

09

10

11

12

13

14

 

final int hash(Object k) {

    int h = hashSeed;

    if (0 != h && k instanceof String) {

        return sun.misc.Hashing.stringHash32((String) k);

    }

 

    h ^= k.hashCode();

 

    // This function ensures that hashCodes that differ only by

    // constant multiples at each bit position have a bounded

    // number of collisions (approximately 8 at default load factor).

    h ^= (h >>> 20) ^ (h >>> 12);

    return h ^ (h >>> 7) ^ (h >>> 4);

}

 

根据上面的注释,我们可以看出,HashMap中使用的hash值,不是Key直接的hashCode,而是经过一系列计算的。

计算hash值的作用就是避免hash碰撞,尽量减少单向链表的产生,因为链表中查找一个元素需要遍历。

indexFor — 计算Key所对应的数组位置

01

02

03

04

 

static int indexFor(int h, int length) {

    // assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";

    return h & (length-1);

}

 

第一次看到这个方法很是不理解,不是应该用 h % length吗?其实这里用了一个非常巧妙的方法来取这个余数。

在计算机中CPU做除法运算、取余运算耗费的CPU周期都比较长,一般几十个CPU周期,而位移运算、位运算只用一个CPU周期。

这样对于性能要求高的地方,就可以用位运算代替普通的除法、取余等运算,JDK源码中有很多这样的例子。

为了能够使用位运算求出这个余数,length必须是2的N次方,这也是我们上面初始化数组大小时要求的,然后使用 h & (length-1),就可以求出余数。具体的算法推导,请自行搜索。

我们用个例子来说明下,如一个Key经过运算的hash为21,length为16:

直接取余运算:21 % 16 = 5

位运算:10101(21) & 01111(16-1) = 00101(5)

哇,这就是计算机运算的魅力,这就是算法的作用。

addEntry — 添加数据

01

02

03

04

05

06

07

08

09

10

 

void addEntry(int hash, K key, V value, int bucketIndex) {

    //如果size大于等于threshold,且数组的这个位置不为null,则扩容数组

    if ((size >= threshold) && (null != table[bucketIndex])) {

        resize(2 * table.length);

        hash = (null != key) ? hash(key) : 0;

        bucketIndex = indexFor(hash, table.length);

    }

 

    createEntry(hash, key, value, bucketIndex);

}

 

threshold:HashMap实际可以存储的Key的个数,如果size大于threshold,说明HashMap已经太饱和了,非常容易发生hash碰撞,导致单向链表的产生。

在inflateTable方法中,我们可以看到

01

 

threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);

 

所以这个值是由HashMap的capacity 和负载因子(loadFactor默认:0.75)计算出来的。

loadFactor越小,相同的capacity就更频繁地扩容,这样的好处是HashMap会很大,产生hash碰撞的几率就更小,但需要的内存也更多,这就是所谓的空间换时间。

在这里也注意,扩容时会直接将原来容量乘以2,满足了length为2的N次方的条件。

createEntry就不多说了,就是将key、value保存到数组响应的位置。

GET

01

02

03

04

05

06

07

08

09

10

11

12

13

14

15

16

17

18

 

final Entry<K,V> getEntry(Object key) {

    if (size == 0) {

        return null;

    }

 

     //用和添加时相同的算法求出hash值

    int hash = (key == null) ? 0 : hash(key);

     //直接从数组的响应位置拿到数据,判断hash相同、key相同,则返回

    for (Entry<K,V> e = table[indexFor(hash, table.length)];

         e != null;

         e = e.next) {

        Object k;

        if (e.hash == hash &&

            ((k = e.key) == key || (key != null && key.equals(k))))

            return e;

    }

    return null;

}

 

获取时非常简单,也非常迅速,添加时做的所有工作都是为快速获取做的工作。

总结

HashMap是一个非常高效的Key、Value数据结构,GET的时间复杂度为:O(1) ~ O(n),我们在使用HashMap时需要注意以下几点:

1. 声明HashMap时最好使用带initialCapacity的构造函数,传入数据的最大size,可以避免内部数组resize;

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/c738de738aaab05c3780f9308d62a528.html