美文网首页
基于JDK1.8的HashMap源码分析

基于JDK1.8的HashMap源码分析

作者: 荆敖晨 | 来源:发表于2019-07-18 14:43 被阅读0次

    特点

    • 根据键的 hashCode 值存储数据

    • 遍历顺序不确定

    • 允许 key 和 value 为 null

    • 非线程安全。如果需要满足线程安全,可以用 Collections.synchronizedMap 方法使 HashMap 具有线程安全的能力,或者使用 ConcurrentHashMap

    存储结构

    • JDK 7 中的 HashMap 采用数组 + 链表的结构来存储数据

    • JDK 8 中的 HashMap 采用数组+链表或树的结构存储数据

    HashMap存储结构.png

    核心属性

    table

    transient Node<K,V>[] table;
    

    这是 HashMap 的核心,HashMap 内部实际上是一个 Node 数组,这个数组的大小必须是 2 的幂次方。
    再看 Node 的实现:

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

    可见,Node 实际上是单链表结构,即 HashMap 中是用单链表结构的数组来存储数据。

    需要注意的是,在 JDK1.8 之后,如果单链表长度大于等于 8 ,则把单链表转换为红黑树结构。

    size

    transient int size;
    

    key-value 键值对的数量,这里不是 table 数组的大小

    capacity

    table 数组的 length,默认是 16,这里需要注意的是,capacity 的值必须是 2 的幂次方。

    loadFactor

    table 能使用的比例,默认是 0.75。

    当负载因子 loadFactor 增大时,每个链表所能容纳的键值对个数越多,遍历链表的时间也就越长,查询、插入等速度也就越慢,适应于内存空间较少且而时间效率要求不高的场景;

    相反,当负载因子减少时,每个链表所能容纳的键值对个数越少,操作 map 的时间较快,但需要增加 table 的长度,所以需要更多的内存,适应于内存空间很多而对时间效率很高的场景;

    默认值是对空间和时间效率的一个平衡选择,一般不需要修改。

    threshold

    int threshold;
    

    size 需要扩容的临界值,当 size 大于等于 threshold 时,就需要进行扩容操作。

    扩容后的 HashMap 容量是之前容量的两倍。

    threshold = capacity * loadFactory

    其中,影响 HashMap 的性能主要有两个因素:initial capacity 和 loadFactory。

    方法

    put

    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }
    
    /*
     * hash:key 的 hash 值
     * onlyIfAbsent:如果为 true 的话,存在 value 的话不进行修改
     * evict:在 HashMap 中没有意义
     */
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
        Node<K,V>[] tab;
        Node<K,V> p;
        int n,i;
        // 这里只有在第一次调用 put 方法时可能会触发
        if ((tab = table) == null || (n == tab.length) == 0)
            n = (tab - resize()).length;
        // 根据 key 的 hash 值找到具体位置,如果这个位置没有值,初始化一个 Node
        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)
                e = ((TreeNode<K,V>) p).putTreeVal(this, tab, hash, key, value);
            // 执行到这里说明该节点属于单链表,且第一个节点 key 不相等
            else {
                for (int binCount = 0; ; ++binCount) {
                    // 循环到链表的最后,插入该值
                    // 在 JDK 1.8中是插入到链表的最后,JDK 1.7中是插入到最前面
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        // TREEIFY_THRESHOLD = 8
                        // 如果新插入的值位于链表的第 8 个,则将链表转换为红黑树结构
                        if (binCount >= TREEIFY_THRESHOLD - 1)
                            treeifBin(tab, hash);
                        break;
                    }
                    // 如果链表中的某个节点的 key 与要插入的 key 相等
                    if (e.hash == hash &&
                       ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    // 如果没有满足上面的条件,则把下一个节点赋给当前节点,下一次循环时再获取下一个节点
                    p = e;
                }
            }
            // 如果 e 不为 null 的话,则说明与 key 相等的 节点
            if (e != null) {    // existing mapping for key
                // 获取这个 key 之前的 value
                V oldValue = e.value;
                // onlyIfAbsent 为 true 的话,就不要改变已有的值
                // 则这里如果 onlyIfAbsent 为 false,或 value 是 null,则将新值替换老的值
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                // HashMap 中不进行操作
                afterNodeAccess(e);
                // 进行覆盖操作时,直接返回之前的值,说明修改值时 modCount 不修改
                return oldValue;
            }
        }
        // 如果是新增值时,需要增加 modCount变量,为迭代器服务
        ++modCount;
        // 如果数组长度大于 threshold 阈值
        if (++size > threshold)
            // 重新散列
            resize();
        // HashMap 中什么都不做
        afterNodeInsertion(evict);
        // 新增值返回 null
        return null;
    }
    

    put 函数大致可以分以下几步:

    1. 如果 table 为 null,则调用 resize() 初始化,默认是创建长度为 16 的数组;
    2. 通过与运算计算对应 hash 值的下标,如果对应下标的位置没有元素,则直接创建一个;
    3. 如果有元素,则说明该 hash 值已经有对应的节点,则再进行判断:
      1. 判断两个冲突的 key 是否相等,,如果相等的话,则到最后执行该节点的替换操作;
      2. 如果 key 不相等,再判断该节点是否为红黑树,如果是的话,则交给红黑树添加此元素;
      3. 如果只是普通链表的话,则遍历链表,判断是否有与给定 key 相等的节点,如果有的话,跳出循环,在后面执行该节点的更新操作,否则在最后添加一个节点,并且判断链表的长度是否已经大于等于 8,满足条件的话,则将链表改为红黑树。
    4. 最后,如果存在该 key 的节点,则执行更新操作
    5. 最后判断当前数组的长度是否已经大于阈值,如果大于则 rehash()

    resize

    resize() 方法用于初始化数组或数组扩容,每次扩容后,容量为原来的 2 倍,并进行数据迁移。

    final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        int oldThr = threshold;
        int newCap, newThr = 0;
        // 如果之前 table 的长度不为 0,则进行扩容
        if (oldCap > 0) {
            // 如果 table 长度超出最大值,则不扩容
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            } else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                      oldCap >= DEFAULT_INITIAL_CAPACITY)
                // 长度允许的情况进行扩容2倍
                newThr = oldThr << 1;
        } else if (oldThr > 0)
            // 这里主要是调用 new HashMap(int initialCapacity) 初始化后,第一次进行初始化的时候
            newCap = oldThr;
        else {
            // 对应调用 new HashMap() 初始化后,第一次进行初始化
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        if (newThr == 0) {
            // 这里只要调用 new HashMap(int initialCapacity) 初始化,执行这里
            // 主要为了校验,newCap 是否超出最大值,且 newCap * loadFactor 是否超出最大值
            float ft = (float)newCap * loadFactor;
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                          (int)ft : Integer.MAX_VALUE);
        }
        threshold = newThr;
        
        Node<K,V> newTab = (Node<K,V>[])new Node[newCap];
        table = newTab;
        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)
                        // e.hash & (newCap - 1) 确定元素所位于的桶下标
                        newTab[e.hash & (newCap - 1)] = e;
                    else if (e instanceof TreeNode)
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    else {
                        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;
                            }
                        } 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;
    }
    

    经过 resize 后,元素的位置要么是在原位置,要么是在原位置再移动 2次幂的位置,即原位置 + oldCap

    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;
        // 判断 table 是否为空,如果为空则返回 null,如果不为空,根据 hash 获得桶下标
        if ((tab = table) != null && (n = tab.length) > 0 &&
           (first = tab[(n - 1) & hash]) != null) {
            // 判断该下标的桶第一个节点是否与当前 key 相等
            if (first.hash == hash &&
               ((k = first.key) == key || (key != null && key.equals(k))))
                return first;
            if ((e = first.next) != null) {
                // 如果这个节点属于红黑树结构
                if (first instanceof TreeNode)
                    return ((TreeNode<K,V>)first).getTreeNode(hash, key);
                // 如果该节点属于单链表,则循环该链表,获得与该 key 相等的节点
                do {
                    if (e.hash == hash &&
                       ((k = e.key) == key || (key != null && key.equals(k))))
                        return e;
                } while ((e = e.next) != null);
            }
        }
        return null;
    }
    

    get函数可以分为下面几步:

    • 根据 key 的 hash 值找到桶所在的下标:hash & (length - 1)
    • 判断该位置的第一个元素是否是要寻找的元素,如果是,则返回
    • 判断该元素类型是否是 TreeNode,如果是,用红黑树的方式取数据
    • 遍历链表,直到找到相等的 key

    小结

    • HashMap 线程不安全,如果需要在并发的情况下同时操作,建议使用 ConcurrentHashMap。
    • 负载因子默认是0.75,可以修改,并且可以大于 1,当设置其值较大时,时间效率低而空间效率高,反之则时间效率高而空间效率低,但是建议一般不要轻易修改,除非情况非常特殊。
    • 扩容是一个特别消耗性能的操作,所以在使用 HashMap 的时候,估算 map 的大小,初始化的时候给一个大致的数值,避免 map 进行频繁的扩容。
    • JDK1.8 中引入了红黑树,大程序优化了 HashMap 的性能。

    相关文章

      网友评论

          本文标题:基于JDK1.8的HashMap源码分析

          本文链接:https://www.haomeiwen.com/subject/ohpahctx.html