zoukankan      html  css  js  c++  java
  • HashMap源码分析

    一、要点

    1. 如何减少哈希碰撞

      1. 将哈希桶长度设置为2的倍数,这样在计算下标时(n-1)& hash 的(n-1)二进制最后一位也会参与运算,

      2. 当Map中元素增加时,势必会造成碰撞的增加,这时候通过扩容来,来减少碰撞

    2. 何时初始化HashMap

      在put值时,初始化hashMap

    3. 哈希桶的寻址方法

      计算下标的算法 (n-1)& hash

    4. 链表何时转红黑树

      当哈希桶中链表长度大于7时,则链表转红黑树,因为红黑树的查找效率更高

    5. 扩容时扩大几倍

      扩大两倍,同时阈值也同样扩大两倍

    6. 为什么hashMap容量都是2的倍数

      1. 计算下标的算法是 (n-1)& hash

      2. indexFor代码,正好解释了为什么HashMap的数组长度要取2的整次幂。因为这样(数组长度-1)正好相当于一个“低位掩码”。“与”操作的结果就是散列值的高位全部归零,只保留低位值,用来做数组下标访问,

      3. 以初始长度16为例,16-1=15。2进制表示是00000000 00000000 00001111。和某散列值做“与”操作如下,结果就是截取了最低的四位值。

      

    7. 讲一下Put的过程

      1. 哈希桶为空的话,调用扩容函数初始化哈希桶,默认长度16

      2. (n-1)& hash计算下标,不发生hash碰撞的话,直接赋值

      3. 发生hash碰撞,如果链头的key值就相同,直接替换,如果是红黑树就进入红黑树对比赋值

      4. 否则遍历链表,比较赋值(比较方式hash+equals)

      5. 最后判断是否需要扩容

    8. 讲一下resize()过程

      1. 判断是否需要初始化哈希桶的容量值、阈值

      2. 遍历老数组,用hash值重新计算下标位置,要么将原来的链表放入低位、要么将要来的链表放入高位

    9. 能否让HashMap同步

      Map m = Collections.synchronizeMap(hashMap);

    10. HashMap的长度为什么设置为2的n次方

      1. 在寻址过中,一般用取余的方式来,这样的效率不高,当容量为2次方时,按位运算&上length-1时,效果和取余相同

      2. 方便扩容时移位操作,效率高,同时扩容后还是2的次方

    二、源码

    hash()方法

    1. 扰动函数就是为了解决hash碰撞的

    key的hash值 异或 hash值的低16位

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

    put()方法

    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }
    
    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;
        // 判断数组中值是否为空,index是 哈希值 & 哈希桶长度-1, 代替模运算
        if ((p = tab[i = (n - 1) & hash]) == null)
            // 数组中对应下标放入值
            tab[i] = newNode(hash, key, value, null);
        else {
            // 数组中对应下标不为空,哈希碰撞
            Node<K,V> e; K k;
            // 判断数组取到的第一个节点的key值是否和要存入的相同
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                // 则把原来的值替换掉
                e = p;
            else if (p instanceof TreeNode)
                // 如果p是红黑树, 则进入红黑树存值的流程
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
                // 否则p是链表, 且第一个节点key值与要存入的不相同, 则对单项链表进行遍历
                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
                            // 链表长度大于8,则将链表转成红黑树
                            treeifyBin(tab, hash);
                        break;
                    }
    
                    //如果e不是null,说明有需要覆盖的节点
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
    
                    // 遍历下一个节点
                    p = e;
                }
            }
    
            // 判断是否找到了与待插入元素的hash值与key值都相同的元素
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        // 记录修改次数
        ++modCount;
        //更新size,并判断是否需要扩容
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

    resize()扩容

    final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;
        // 获取旧的数组的长度,和阈值
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        int oldThr = threshold;
        int newCap, newThr = 0;
        // 旧的容量大于0的情况
        if (oldCap > 0) {
            // 如果数组长度等于最大容量值,则不扩容直接返回
            if (oldCap >= MAXIMUM_CAPACITY) {
                // 设置阈值为2的31次方-1
                threshold = Integer.MAX_VALUE;
                // 不扩容了,直接返回旧的数组
                return oldTab;
            }
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                        oldCap >= DEFAULT_INITIAL_CAPACITY)
                // 如果容量变为2倍后小于最大容量,且大于等于默认容量16时
                // 阈值也扩大一倍
                newThr = oldThr << 1; // double threshold
        }
        //如果当前表是空的,但是有阈值。代表是初始化时指定了容量、阈值的情况
        else if (oldThr > 0) // initial capacity was placed in threshold
            newCap = oldThr;
        else {               // zero initial threshold signifies using defaults
            // 当数组还未被初始化时,设置默认容量和阈值
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        if (newThr == 0) { // 如果新的阈值是0,对应的是  当前表是空的,但是有阈值的情况
            float ft = (float)newCap * loadFactor;
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                        (int)ft : Integer.MAX_VALUE);
        }
        threshold = newThr;
        // 扩容重新创建一个大小为2倍的数组
        @SuppressWarnings({"rawtypes","unchecked"})
        Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
        // 更新哈希桶引用
        table = newTab;
        if (oldTab != null) {
            // 循环将老数组中的放入新数组
            for (int j = 0; j < oldCap; ++j) {
                // 当前节点e
                Node<K,V> e;
                if ((e = oldTab[j]) != null) {
                    // 将原哈希桶置空,以便GC
                    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);
                    // 如果发生了Hash碰撞, 节点小于8,要遍历节点,依次放入新的节点
                    else { // preserve order
                        //因为扩容是容量翻倍,所以原链表上的每个节点,现在可能存放在原来的下标,即low位, 或者扩容后的下标,即high位。 high位=  low位+原哈希桶容量
                         //低位链表的头结点、尾节点
                        Node<K,V> loHead = null, loTail = null;
                        // 高位链表的头结点、尾结点
                        Node<K,V> hiHead = null, hiTail = null;
                        
                        Node<K,V> next;
                        do {
                            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;
                        }
                    }
                }
            }
        }
        return newTab;
    }

    get方法

    public V get(Object key) {
        Node<K,V> e;
        return (e = getNode(hash(key), key)) == null ? null : e.value;
    }
    final Node<K,V> getNode(int hash, Object key) {
        Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
        // 判断数组,且数组中链表或红黑树第一个节点不为空
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (first = tab[(n - 1) & hash]) != null) {
            if (first.hash == hash && // always check first node
                ((k = first.key) == key || (key != null && key.equals(k))))
                // 若第一个节点的key就相同则直接放回第一个节点的值,若map中值不多,大多情况,第一个就是想要的值
                return first;
            if ((e = first.next) != null) {
                // 判断是否是红黑树, 是红黑树就进入红黑树中找值
                if (first instanceof TreeNode)
                    return ((TreeNode<K,V>)first).getTreeNode(hash, key);
                do {
                    // 遍历节点去找
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        return e;
                } while ((e = e.next) != null);
            }
        }
        return null;
    }

    三、疑问点

    1. 扩容时,hiTail = e表示什么意思

  • 相关阅读:
    ServerSuperIO 3.5版本的体系结构,以及未来规划的几点思考
    《连载 | 物联网框架ServerSuperIO教程》- 18.集成OPC Client,及使用步骤。附:3.5 发布与更新说明。
    《连载 | 物联网框架ServerSuperIO教程》- 17.集成Golden实时数据库,高并发保存测点数据。附:3.4 发布与版本更新说明。
    《连载 | 物联网框架ServerSuperIO教程》- 16.集成OPC Server,及使用步骤。附:3.3 发布与版本更新说明。
    [祝贺] 东方国信集团的钢铁大数据和工业节能两个案例入选工信部工业互联网优秀案例
    《连载 | 物联网框架ServerSuperIO教程》- 15.数据持久化接口的使用。附:3.2发布与版本更新说明。
    物联网建设中通讯互联层的终极解决方案
    hadoop 2.7.2 + zookeeper 高可用集群部署
    开源物联网框架ServerSuperIO 3.0正式发布(C#),跨平台:Win&Win10 Iot&Ubuntu&Ubuntu Mate,一套设备驱动跨平台挂载,附:开发套件和教程。
    《连载 | 物联网框架ServerSuperIO教程》- 14.配制工具介绍,以及设备驱动、视图驱动、服务实例的挂载
  • 原文地址:https://www.cnblogs.com/milicool/p/11435244.html
Copyright © 2011-2022 走看看