zoukankan      html  css  js  c++  java
  • 算法与数据结构 (八) HashMap源码

    一  存储结构 

    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()也可能丢失更新

  • 相关阅读:
    tips 前端 阻止 浏览器缓存静态资源
    tips 前端 各个设备的页面尺寸的media query 与页面高度的经验总结
    算法-第四版-练习1.3.28解答
    事件循环(event loop) && 任务队列 (task queue) && $nextTick
    element -ui之<el-table>
    node.js(三 --- stream 管道流 模块化 函数)
    node.js(二 --- events 、buffer、)
    ajax请求的所有状态码详解
    element-ui中el-tree的父元素设置好宽度后不能被子内容撑开的问题
    java获取前端请求字段的方法
  • 原文地址:https://www.cnblogs.com/caijiwdq/p/11072465.html
Copyright © 2011-2022 走看看