算法与数据结构 (八) HashMap源码

时间:2022-11-24 10:17:59

一  存储结构 

static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
transient Node<K,V>[] table;

内部存储的单元如上所示,整体上就是数组加链表的桶状结构。

二  put操作

put(key,value)内部调用的是putVal() 下面是源码  jdk1.8采用的是尾插法  

 final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;     //如果是第一个元素 就初始化数组  懒加载方式
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);  // 如果当前位置 还没有元素 就初始化当前的位置  这里确定位置的方法 是&运算
        else {   //查找节点(键值为key)
            Node<K,V> e; K k;
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;      
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }  
//以下代码好像是为了linkedhashmap服务的 if (e != null) { // existing mapping for key V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); return oldValue; } } ++modCount;
// 如果容量超了 就扩容 if (++size > threshold) resize(); afterNodeInsertion(evict); return null; }

  代码除了红黑树的部分引出两个部分:1. 为什么采取hash & 长度-1 的方式找数组位置  2.  如何扩容

 

三  初始化和扩容 

定容量的方法:

static final int tableSizeFor(int cap) {
        int n = cap - 1;
        n |= n >>> 1;
        n |= n >>> 2;
        n |= n >>> 4;
        n |= n >>> 8;
        n |= n >>> 16;
        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
    }

  对于构造函数中传入的整数,进行如下操作。结论就是得到的数,是大于等于传入值的最小 2 的幂。也就是传入64 就是64 ,传入127 就是 128。

这算法想起来不好想,但看代码还是好理解。对于65这种,减去1,就是 100 0000  第一次运算 是110 0000 第二次: 111 1000,结果变成1111111(全1) 最后加上1  必然是2 的整次幂。

这也解决了 hash运算为什么用   hash & n-1,因为&的效率大于除法和取余运算,而且由于是2的整次幂,按位与和取余操作效果一样,但是速度更快。

 

此外 node节点中的hash 值 ,是这样得到的,也就是原来key hashcode高16位和hashcode亦或。为的是增加高16位的参与感,减少hash冲突,如果key的hashcode 是  0A01 0B01 0C01 如果直接进行 按位与操作,那么他们都被映射到同一个位置,而经过这么一轮操作会,会减少hash碰撞的情况。

static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

而扩容主要是resize方法 主要是这段代码 ,新的数组是旧的数组的二倍,如下是旧的数据迁移到新数组中
if (oldTab != null) {
            for (int j = 0; j < oldCap; ++j) {
                Node<K,V> e;   
                if ((e = oldTab[j]) != null) {    
                    oldTab[j] = null;
                    if (e.next == null)       //第一种情况,节点就一个元素
                        newTab[e.hash & (newCap - 1)] = e;  //当前节点在新节点中的位置
                    else if (e instanceof TreeNode)   //第二种情况,节点是红黑树
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    else { // preserve order           //第三种情况 节点是链表
                        Node<K,V> loHead = null, loTail = null;
                        Node<K,V> hiHead = null, hiTail = null;
                        Node<K,V> next;
                        do {    //分成两类 比如旧容量是16  对于 hash 17 1 他们都在下标为1的位置,但是 &16 ,要么是0 要么是1 。这就是分类的标准  为0 的放在新的数组的1的位置,为1的放到17 的位置
                            next = e.next;
                            if ((e.hash & oldCap) == 0) {
                                if (loTail == null)
                                    loHead = e;
                                else
                                    loTail.next = e;
                                loTail = e;
                            }
                            else {
                                if (hiTail == null)
                                    hiHead = e;
                                else
                                    hiTail.next = e;
                                hiTail = e;
                            }
                        } while ((e = next) != null);
                        if (loTail != null) {
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }

  四 总结

1 .hash值的算法,键的hash值与 高16亦或,保留高位信息

        ---映射下标值 &(容量-1)得到实际的

2.  保证容量是2的幂 采取按位或  

3. .hashMap的结构 

                 在1.8做出的改变,数组加红黑树 链表大于8就会变树,

                  变树的代价比较高,所以还增加了一个条件,就是数组大小要大于64,否则采取扩容的方法,解决链表过长的问题。

4.hashmap可以插入空值键

                有单独的方法,永远插在第一个链表,也就是数组下标为0的位置

5.遍历方法 

           map.entrySet()   这是拿到了所有的键值对

            keyset()    这个是拿到了所有的键值

6.并发下的问题

             put()丢失更新

             resize()也可能丢失更新