腾讯云双十一,服务器_【鸠合系列】- 深入浅出剖析HashMap

  • 腾讯云双十一,服务器_【鸠合系列】- 深入浅出剖析HashMap已关闭评论
  • 123 人浏览
  • A+
所属分类:首页

一、择要

在鸠合系列的第一章,我们相识到,Map的完成类有HashMap、LinkedHashMap、TreeMap、IdentityHashMap、WeakHashMap、Hashtable、Properties等等。

腾讯云双十一,服务器_【鸠合系列】- 深入浅出剖析HashMap

关于HashMap,一向都是一个异常热点的话题,只需你出去口试,我保证肯定少不了它!

本文主要连系JDK1.7和JDK1.8的区分,就HashMap的数据构造和完胜利用,举行深入探讨,空话也不多说了,直奔主题!

二、简介

在递次编程的时刻,HashMap是一个运用异常频仍的容器类,它许可键值都放入null元素。除该类要领未完成同步外,其他跟Hashtable大抵雷同,但跟TreeMap差别,该容器不保证元素递次,依据须要该容器大概会对元素从新哈希,元素的递次也会被从新打散,因而差别时候迭代统一个HashMap的递次大概会差别。

HashMap容器,实质照样一个哈希数组构造,然则在元素插进去的时刻,存在发作hash争执的大概性;

关于发作Hash争执的状况,争执有两种完成体式格局,一种开放地点体式格局(当发作hash争执时,就继承以此继承寻觅,直到找到没有争执的hash值),另一种是拉链体式格局(将争执的元素放入链表)Java HashMap采纳的就是第二种体式格局,拉链法。

在jdk1.7中,HashMap重假如由数组+链表构成,当发作hash争执的时刻,就将争执的元素放入链表中。

从jdk1.8入手下手,HashMap重假如由数组+链表+红黑树完成的,比拟jdk1.7而言,多了一个红黑树完成。当链表长度凌驾8的时刻,就将链表变成红黑树,如图所示。

腾讯云双十一,服务器_【鸠合系列】- 深入浅出剖析HashMap

关于红黑树的完成,因为篇幅太长,在《鸠合系列》文章中红黑树设想,也有所引见,这里就不在细致引见了。

三、源码剖析

直接翻开HashMap的源码剖析,能够看到,主要有5个症结参数:

  • threshold:示意容器所能包容的key-value对极限。
  • loadFactor:负载因子。
  • modCount:纪录修正次数。
  • size:示意现实存在的键值对数目。
  • table:一个哈希桶数组,键值对就寄存在内里。
public class HashMap<K,V> extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable {
    
    //所能包容的key-value对极限
    int threshold;
    
    //负载因子
    final float loadFactor;
    
    //纪录修正次数
    int modCount;
    
    //现实存在的键值对数目
    int size;
    
    //哈希桶数组
    transient Node<K,V>[] table;
}

接着来看看Node这个类,NodeHashMap的一个内部类,完成了Map.Entry接口,实质是就是一个映照(键值对)

static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;//hash值
        final K key;//k键
        V value;//value值
        Node<K,V> next;//链表中下一个元素
}

在HashMap的数据构造中,有两个参数能够影响HashMap的机能:初始容量(inital capacity)负载因子(load factor)

初始容量(inital capacity)是指table的初始长度length(默许值是16);
负载因子(load factor)用指自动扩容的临界值(默许值是0.75);

thresholdHashMap所能包容的最大数据量的Node(键值对)个数,盘算公式threshold = capacity * Load factor。当entry的数目凌驾capacity*load_factor时,容器将自动扩容并从新哈希,扩容后的HashMap容量是之前容量的两倍所以数组的长度老是2的n次方

初始容量负载因子也能够修正,详细完成体式格局,能够在对象初始化的时刻,指定参数,比方:

Map map = new HashMap(int initialCapacity, float loadFactor);

然则,默许的负载因子0.75是对空间和时候效力的一个均衡挑选,发起人人不要修正,除非在时候和空间比较特别的状况下,假如内存空间许多而又对时候效力请求很高,能够下降负载因子Load factor的值;相反,假如内存空间慌张而对时候效力请求不高,能够增添负载因子loadFactor的值,这个值能够大于1。 同时,关于插进去元素较多的场景,能够将初始容量设大,削减从新哈希的次数。

HashMap的内部功用完成有许多,本文主要从以下几点,举行逐渐剖析。

  • 经由历程K猎取数组下标;
  • put要领的细致实行;
  • resize扩容历程;
  • get要领猎取参数值;
  • remove删除元素;

3.1、经由历程K猎取数组下标

不论增添、删除照样查找键值对,定位到数组的位置都是很症结的第一步,翻开hashMap的恣意一个增添、删除、查找要领,从源码能够看出,经由历程key猎取数组下标,主要做了3步操纵,个中length指的是容器数组的大小。

腾讯云双十一,服务器_【鸠合系列】- 深入浅出剖析HashMap

源码部份:

/**猎取hash值要领*/
static final int hash(Object key) {
     int h;
     // h = key.hashCode() 为第一步 取hashCode值(jdk1.7)
     // h ^ (h >>> 16)  为第二步 高位介入运算(jdk1.7)
     return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);//jdk1.8
}
/**猎取数组下标要领*/
static int indexFor(int h, int length) {
    //jdk1.7的源码,jdk1.8没有这个要领,然则完成道理一样的
     return h & (length-1);  //第三步 取模运算
}

3.2、put要领的细致实行

put(K key, V value)要领是将指定的key, value对增加到map里。该要领起首会对map做一次查找,看是不是包括该K,假如已包括则直接返回;假如没有找到,则将元素插进去容器。详细插进去历程以下:

腾讯云双十一,服务器_【鸠合系列】- 深入浅出剖析HashMap

详细实行步骤

  • 1、推断键值对数组table[i]是不是为空或为null,不然实行resize()举行扩容;
  • 2、依据键值key盘算hash值获得插进去的数组索引i,假如table[i]==null,直接新建节点增加;
  • 3、当table[i]不为空,推断table[i]的首个元素是不是和传入的key一样,假如雷同直接掩盖value;
  • 4、推断table[i] 是不是为treeNode,即table[i] 是不是是红黑树,假如是红黑树,则直接在树中插进去键值对;
  • 5、遍历table[i],推断链表长度是不是大于8,大于8的话把链表转换为红黑树,在红黑树中实行插进去操纵,不然举行链表的插进去操纵;遍历历程当中若发明key已存在直接掩盖value即可;
  • 6、插进去胜利后,推断现实存在的键值对数目size是不是超多了最大容量threshold,假如凌驾,举行扩容操纵;

put要领源码部份

/**
 * 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;
        //1、推断数组table是不是为空或为null
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        //2、推断数组下标table[i]==null
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K,V> e; K k;
            //3、推断table[i]的首个元素是不是和传入的key一样
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            //4、推断table[i] 是不是为treeNode
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
                //5、遍历table[i],推断链表长度是不是大于8
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        //长度大于8,转红黑树构造
                        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;
                }
            }
            //传入的K元素已存在,直接掩盖value
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        //6、推断size是不是超越最大容量
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
}

个中,与jdk1.7有区分的处所,第4步新增了红黑树插进去要领,源码部份:

/**
   * 红黑树的插进去操纵
   */
final TreeNode<K,V> putTreeVal(HashMap<K,V> map, Node<K,V>[] tab,
                                       int h, K k, V v) {
            Class<?> kc = null;
            boolean searched = false;
            TreeNode<K,V> root = (parent != null) ? root() : this;
            for (TreeNode<K,V> p = root;;) {
                //dir:遍历的方向, ph:p节点的hash值
                int dir, ph; K pk;
                //红黑树是依据hash值来推断大小
                // -1:左孩子方向 1:右孩子方向
                if ((ph = p.hash) > h)
                    dir = -1;
                else if (ph < h)
                    dir = 1;
                //假如key存在的话就直接返回当前节点
                else if ((pk = p.key) == k || (k != null && k.equals(pk)))
                    return p;
                //假如当前插进去的范例和正在比较的节点的Key是Comparable的话,就直接经由历程此接口比较
                else if ((kc == null &&
                          (kc = comparableClassFor(k)) == null) ||
                         (dir = compareComparables(kc, k, pk)) == 0) {
                    if (!searched) {
                        TreeNode<K,V> q, ch;
                        searched = true;
                        //尝试在p的左子树或许右子树中找到了目的元素
                        if (((ch = p.left) != null &&
                             (q = ch.find(h, k, kc)) != null) ||
                            ((ch = p.right) != null &&
                             (q = ch.find(h, k, kc)) != null))
                            return q;
                    }
                    //猎取遍历的方向
                    dir = tieBreakOrder(k, pk);
                }
                //上面的一切if-else推断都是在推断下一次举行遍历的方向,即dir
                TreeNode<K,V> xp = p;
                //当下面的if推断进去今后就代表找到了目的操纵元素,即xp
                if ((p = (dir <= 0) ? p.left : p.right) == null) {
                    Node<K,V> xpn = xp.next;
                    //插进去新的元素
                    TreeNode<K,V> x = map.newTreeNode(h, k, v, xpn);
                    if (dir <= 0)
                        xp.left = x;
                    else
                        xp.right = x;
                    //因为TreeNode今后大概退化成链表,在这里须要保护链表的next属性
                    xp.next = x;
                    //完成节点插进去操纵
                    x.parent = x.prev = xp;
                    if (xpn != null)
                        ((TreeNode<K,V>)xpn).prev = x;
                    //插进去操纵完成今后就要举行肯定的调解操纵了
                    moveRootToFront(tab, balanceInsertion(root, x));
                    return null;
                }
       }
}

3.3、resize扩容历程

在说jdk1.8的HashMap动态扩容之前,我们先来相识一下jdk1.7的HashMap扩容完成,因为jdk1.8代码完成比Java1.7庞杂了不止一倍,重假如Java1.8引入了红黑树设想,然则完成头脑迥然差别!

3.3.1、jdk1.7的扩容完成

腾讯云双十一,服务器_【鸠合系列】- 深入浅出剖析HashMap

源码部份

/**
  * JDK1.7扩容要领
  * 传入新的容量
  */
void resize(int newCapacity) {
    //援用扩容前的Entry数组
    Entry[] oldTable = table;
    int oldCapacity = oldTable.length;
    //扩容前的数组大小假如已到达最大(2^30)了
    if (oldCapacity == MAXIMUM_CAPACITY) {
        //修正阈值为int的最大值(2^31-1),如许今后就不会扩容了
        threshold = Integer.MAX_VALUE;
        return;
    }
    //初始化一个新的Entry数组
    Entry[] newTable = new Entry[newCapacity];
    //将数据转移到新的Entry数组里,这里包括最主要的从新定位
    transfer(newTable);
    //HashMap的table属性援用新的Entry数组
    table = newTable;
    threshold = (int) (newCapacity * loadFactor);//修正阈值
}

transfer复制数组要领,源码部份:

//遍历每一个元素,按新的容量举行rehash,放到新的数组上
void transfer(Entry[] newTable) {
    //src援用了旧的Entry数组
    Entry[] src = table;
    int newCapacity = newTable.length;
    for (int j = 0; j < src.length; j++) {
        //遍历旧的Entry数组
        Entry<K, V> e = src[j];
        //获得旧Entry数组的每一个元素
        if (e != null) {
            //开释旧Entry数组的对象援用(for轮回后,旧的Entry数组不再援用任何对象)
            src[j] = null;
            do {
                Entry<K, V> next = e.next;
                //从新盘算每一个元素在数组中的位置
                //完成逻辑,也是上文谁人取模运算要领
                int i = indexFor(e.hash, newCapacity);
                //标记数组
                e.next = newTable[i];
                //将元素放在数组上
                newTable[i] = e;
                //接见下一个Entry链上的元素,轮回遍历
                e = next;
            } while (e != null);
        }  
    }  
}

jdk1.7扩容总结:newTable[i]的援用赋给了e.next,也就是运用了单链表的头插进去体式格局,统一名置上新元素总会被放在链表的头部位置;如许先放在一个索引上的元素终会被放到Entry链的尾部(假如发作了hash争执的话),这一点和Jdk1.8有区分。在旧数组中统一条Entry链上的元素,经由历程从新盘算索引位置后,有大概被放到了新数组的差别位置上。

3.3.2、jdk1.8的扩容完成

腾讯云双十一,服务器_【鸠合系列】- 深入浅出剖析HashMap

源码以下

final Node<K,V>[] resize() {
        //援用扩容前的node数组
        Node<K,V>[] oldTab = table;
        //旧的容量
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        //旧的阈值
        int oldThr = threshold;
        //新的容量、阈值初始化为0
        int newCap, newThr = 0;
        if (oldCap > 0) {
            //假如旧容量已凌驾最大容量,让阈值也即是最大容量,今后不再扩容
                threshold = Integer.MAX_VALUE;
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            // 没凌驾最大值,就扩大为原本的2倍
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                //假如旧容量翻倍没有凌驾最大值,且旧容量不小于初始化容量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
            //0的时刻运用默许值初始化
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        //盘算新阈值,假如新容量或新阈值大于即是最大容量,则直接运用最大值作为阈值,不再扩容
        if (newThr == 0) {
            float ft = (float)newCap * loadFactor;
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }
        //设置新阈值
        threshold = newThr;
        @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) {
                //以一个新的节点援用当前节点,然后开释原本的节点的援用
                Node<K,V> e;
                if ((e = oldTab[j]) != null) {
                    oldTab[j] = null;
                    //假如e没有next节点,证实这个节点上没有hash争执,则直接把e的援用给到新的数组位置上
                    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 {
                        // 链表优化重hash的代码块
                        Node<K,V> loHead = null, loTail = null;
                        Node<K,V> hiHead = null, hiTail = null;
                        Node<K,V> next;
                        //从这条链表上第一个元素入手下手轮询,假如当前元素新增的bit是0,则放在当前这条链表上,假如是1,则放在"j+oldcap"这个位置上,生成“低位”和“高位”两个链表
                        do {
                            next = e.next;
                            if ((e.hash & oldCap) == 0) {
                                if (loTail == null)
                                    loHead = e;
                                else
                                    //元素是不停的加到尾部的,不会像1.7内里一样会倒序
                                    loTail.next = e;
                                //新增的元素永远是尾元素
                                loTail = e;
                            }
                            else {
                                //高位的链表与低位的链表处置惩罚逻辑一样,不停的把元素加到链表尾部
                                if (hiTail == null)
                                    hiHead = e;
                                else
                                    hiTail.next = e;
                                hiTail = e;
                            }
                        } while ((e = next) != null);
                        //低位链表放到j这个索引的位置上
                        if (loTail != null) {
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                        //高位链表放到(j+oldCap)这个索引的位置上
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
}

1.7与1.8处置惩罚逻辑迥然差别,区分主要照样在树节点的破裂((TreeNode<K,V>)e).split()这个要领上

/**
 * 红黑树破裂要领
 */
final void split(HashMap<K,V> map, Node<K,V>[] tab, int index, int bit) {
            //当前这个节点的援用,即这个索引上的树的根节点
            TreeNode<K,V> b = this;
            // Relink into lo and hi lists, preserving order
            TreeNode<K,V> loHead = null, loTail = null;
            TreeNode<K,V> hiHead = null, hiTail = null;
            //高位低位的初始树节点个数都设成0
            int lc = 0, hc = 0;
            for (TreeNode<K,V> e = b, next; e != null; e = next) {
                next = (TreeNode<K,V>)e.next;
                e.next = null;
                //bit=oldcap,这里推断新bit位是0照样1,假如是0就放在低位树上,假如是1就放在高位树上,这里先是一个双向链表
                if ((e.hash & bit) == 0) {
                    if ((e.prev = loTail) == null)
                        loHead = e;
                    else
                        loTail.next = e;
                    loTail = e;
                    ++lc;
                }
                else {
                    if ((e.prev = hiTail) == null)
                        hiHead = e;
                    else
                        hiTail.next = e;
                    hiTail = e;
                    ++hc;
                }
            }

            if (loHead != null) {
                if (lc <= UNTREEIFY_THRESHOLD)
                    //!!!假如低位的链表长度小于阈值6,则把树变成链表,并放到新数组中j索引位置
                    tab[index] = loHead.untreeify(map);
                else {
                    tab[index] = loHead;
                    //高位不为空,举行红黑树转换
                    if (hiHead != null) // (else is already treeified)
                        loHead.treeify(tab);
                }
            }
            if (hiHead != null) {
                if (hc <= UNTREEIFY_THRESHOLD)
                    tab[index + bit] = hiHead.untreeify(map);
                else {
                    tab[index + bit] = hiHead;
                    if (loHead != null)
                        hiHead.treeify(tab);
                }
            }
}

untreeify要领,将树转变成单向链表

/**
 * 将树转变成单向链表
 */
final Node<K,V> untreeify(HashMap<K,V> map) {
            Node<K,V> hd = null, tl = null;
            for (Node<K,V> q = this; q != null; q = q.next) {
                Node<K,V> p = map.replacementNode(q, null);
                if (tl == null)
                    hd = p;
                else
                    tl.next = p;
                tl = p;
            }
            return hd;
}

treeify要领,将链表转换为红黑树,会依据红黑树特征举行色彩转换、左旋、右旋等

/**
 * 链表转换为红黑树,会依据红黑树特征举行色彩转换、左旋、右旋等
 */
final void treeify(Node<K,V>[] tab) {
            TreeNode<K,V> root = null;
            for (TreeNode<K,V> x = this, next; x != null; x = next) {
                next = (TreeNode<K,V>)x.next;
                x.left = x.right = null;
                if (root == null) {
                    x.parent = null;
                    x.red = false;
                    root = x;
                }
                else {
                    K k = x.key;
                    int h = x.hash;
                    Class<?> kc = null;
                    for (TreeNode<K,V> p = root;;) {
                        int dir, ph;
                        K pk = p.key;
                        if ((ph = p.hash) > h)
                            dir = -1;
                        else if (ph < h)
                            dir = 1;
                        else if ((kc == null &&
                                  (kc = comparableClassFor(k)) == null) ||
                                 (dir = compareComparables(kc, k, pk)) == 0)
                            dir = tieBreakOrder(k, pk);

                        TreeNode<K,V> xp = p;
                        if ((p = (dir <= 0) ? p.left : p.right) == null) {
                            x.parent = xp;
                            if (dir <= 0)
                                xp.left = x;
                            else
                                xp.right = x;
                            //举行左旋、右旋调解
                            root = balanceInsertion(root, x);
                            break;
                        }
                    }
                }
            }
            moveRootToFront(tab, root);
}

jdk1.8在举行从新扩容今后,会从新盘算hash值,因为n变成2倍,假定初始 tableSize = 4 要扩容到 8 来讲就是 0100 到 1000 的变化(左移一名就是 2 倍),在扩容中只用推断原本的 hash 值与左挪动的一名(newtable 的值)按位与操纵是 0 或 1 就行,0 的话索引就稳定,1 的话索引变成原索引 + oldCap;

其完成以下流程图所示:

腾讯云双十一,服务器_【鸠合系列】- 深入浅出剖析HashMap

能够瞥见,因为 hash 值原本就是随机性的,所以 hash 按位与上 newTable 获得的 0(扩容前的索引位置)和 1(扩容前索引位置加上扩容前数组长度的数值索引处)就是随机的,所以扩容的历程就能把之前哈希争执的元素再随机的散布到差别的索引去,这算是 JDK1.8 的一个优化点。

另外,JDK1.7中rehash的时刻,旧链表迁徙新链表的时刻,假如在新表的数组索引位置雷同,则链表元素会颠倒,然则从上图能够看出,JDK1.8不会颠倒。

同时,因为 JDK1.7 中发作哈希争执时仅仅采纳了链表构造存储争执元素,所以扩容时仅仅是从新盘算其存储位置罢了。而 JDK1.8 中为了机能在统一索引处发作哈希争执到肯定水平时,链表构造会转换为红黑数构造存储争执元素,故在扩容时假如当前索引中元素构造是红黑树且元素个数小于链表复原阈值时就会把树形构造减少或直接复原为链表构造(其完成就是上面代码片断中的 split() 要领)。

3.4、get要领猎取参数值

get(Object key)要领依据指定的key值返回对应的value,getNode(hash(key), key))获得响应的Node对象e,然后返回e.value。因而getNode()是算法的中心。

腾讯云双十一,服务器_【鸠合系列】- 深入浅出剖析HashMap

get要领源码部份:

/**
  * JDK1.8 get要领
  * 经由历程key猎取参数值
  */
public V get(Object key) {
        Node<K,V> e;
        return (e = getNode(hash(key), key)) == null ? null : e.value;
}

经由历程hash值和key猎取节点Node要领,源码部份:

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) {
            //1、推断第一个元素是不是与key婚配
            if (first.hash == hash &&
                ((k = first.key) == key || (key != null && key.equals(k))))
                return first;
            if ((e = first.next) != null) {
                //2、推断链表是不是红黑树构造
                if (first instanceof TreeNode)
                    return ((TreeNode<K,V>)first).getTreeNode(hash, key);
                //3、假如不是红黑树构造,直接轮回推断
                do {
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        return e;
                } while ((e = e.next) != null);
            }
        }
        return null;
}

在红黑树中找到指定k的TreeNode,源码部份:

/**
  * 这内里状况分许多中,重假如因为斟酌了hash雷同然则key值差别的状况,查找的最中心照样落在key值上
  */
final TreeNode<K,V> find(int h, Object k, Class<?> kc) {
            TreeNode<K,V> p = this;
            do {
                int ph, dir; K pk;
                TreeNode<K,V> pl = p.left, pr = p.right, q;
                //推断要查询元素的hash是不是在树的左侧
                if ((ph = p.hash) > h)
                    p = pl;
                //推断要查询元素的hash是不是在树的右侧
                else if (ph < h)
                    p = pr;
                //查询元素的hash与当前树节点hash雷同状况
                else if ((pk = p.key) == k || (k != null && k.equals(pk)))
                    return p;
                //上面的三步都是一般的在二叉查找树中寻觅对象的要领
                //假如hash相称,然则内容却不相称
                else if (pl == null)
                    p = pr;
                else if (pr == null)
                    p = pl;
                 //假如能够依据compareTo举行比较的话就依据compareTo举行比较
                else if ((kc != null ||
                          (kc = comparableClassFor(k)) != null) &&
                         (dir = compareComparables(kc, k, pk)) != 0)
                    p = (dir < 0) ? pl : pr;
                //依据compareTo的结果在右孩子上继承查询
                else if ((q = pr.find(h, k, kc)) != null)
                    return q;
                //依据compareTo的结果在左孩子上继承查询
                else
                    p = pl;
            } while (p != null);
            return null;
}

get要领,起首经由历程hash()函数获得对应数组下标,然后顺次推断。

  • 1、推断第一个元素与key是不是婚配,假如婚配就返回参数值;
  • 2、推断链表是不是红黑树,假如是红黑树,就进入红黑树要领猎取参数值;
  • 3、假如不是红黑树构造,直接轮回推断,直到猎取参数为止;

3.5、remove删除元素

remove(Object key)的作用是删除key值对应的Node,该要领的详细逻辑是在removeNode(hash(key), key, null, false, true)里完成的。

腾讯云双十一,服务器_【鸠合系列】- 深入浅出剖析HashMap

remove要领,源码部份:

/**
  * JDK1.8 remove要领
  * 经由历程key移除对象
  */
public V remove(Object key) {
        Node<K,V> e;
        return (e = removeNode(hash(key), key, null, false, true)) == null ?
            null : e.value;
}

经由历程key移除Node节点要领,源码部份:

/**
  * 经由历程key移除Node节点
  */
final Node<K,V> removeNode(int hash, Object key, Object value,
                               boolean matchValue, boolean movable) {
        Node<K,V>[] tab; Node<K,V> p; int n, index;
        //1、推断要删除的元素,是不是存在
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (p = tab[index = (n - 1) & hash]) != null) {
            Node<K,V> node = null, e; K k; V v;
            //2、推断第一个元素是不是是我们要找的元素
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                node = p;
            else if ((e = p.next) != null) {
                //3、推断当前争执链表是不是红黑树构造
                if (p instanceof TreeNode)
                    node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
                else {
                    //4、轮回在链表中找到须要删除的元素
                    do {
                        if (e.hash == hash &&
                            ((k = e.key) == key ||
                             (key != null && key.equals(k)))) {
                            node = e;
                            break;
                        }
                        p = e;
                    } while ((e = e.next) != null);
                }
            }
            //上面的逻辑,基本都是为了找到要删除元素的节点
            if (node != null && (!matchValue || (v = node.value) == value ||
                                 (value != null && value.equals(v)))) {
                //5、假如当前争执链表构造是红黑树,实行红黑树删除要领
                if (node instanceof TreeNode)
                    ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
                else if (node == p)
                    tab[index] = node.next;
                else
                    p.next = node.next;
                ++modCount;
                --size;
                afterNodeRemoval(node);
                return node;
            }
        }
        return null;
}

removeTreeNode移除红黑树节点要领,源码部份:

final void removeTreeNode(HashMap<K,V> map, Node<K,V>[] tab,
                                  boolean movable) {
            int n;
            if (tab == null || (n = tab.length) == 0)
                return;
            int index = (n - 1) & hash;
            TreeNode<K,V> first = (TreeNode<K,V>)tab[index], root = first, rl;
            TreeNode<K,V> succ = (TreeNode<K,V>)next, pred = prev;
            if (pred == null)
                tab[index] = first = succ;
            else
                pred.next = succ;
            if (succ != null)
                succ.prev = pred;
            if (first == null)
                return;
            if (root.parent != null)
                root = root.root();
            if (root == null || root.right == null ||
                (rl = root.left) == null || rl.left == null) {
                tab[index] = first.untreeify(map);  // too small
                return;
            }
            TreeNode<K,V> p = this, pl = left, pr = right, replacement;
            if (pl != null && pr != null) {
                TreeNode<K,V> s = pr, sl;
                while ((sl = s.left) != null) // find successor
                    s = sl;
                boolean c = s.red; s.red = p.red; p.red = c; // swap colors
                TreeNode<K,V> sr = s.right;
                TreeNode<K,V> pp = p.parent;
                if (s == pr) { // p was s's direct parent
                    p.parent = s;
                    s.right = p;
                }
                else {
                    TreeNode<K,V> sp = s.parent;
                    if ((p.parent = sp) != null) {
                        if (s == sp.left)
                            sp.left = p;
                        else
                            sp.right = p;
                    }
                    if ((s.right = pr) != null)
                        pr.parent = s;
                }
                p.left = null;
                if ((p.right = sr) != null)
                    sr.parent = p;
                if ((s.left = pl) != null)
                    pl.parent = s;
                if ((s.parent = pp) == null)
                    root = s;
                else if (p == pp.left)
                    pp.left = s;
                else
                    pp.right = s;
                if (sr != null)
                    replacement = sr;
                else
                    replacement = p;
            }
            else if (pl != null)
                replacement = pl;
            else if (pr != null)
                replacement = pr;
            else
                replacement = p;
            if (replacement != p) {
                TreeNode<K,V> pp = replacement.parent = p.parent;
                if (pp == null)
                    root = replacement;
                else if (p == pp.left)
                    pp.left = replacement;
                else
                    pp.right = replacement;
                p.left = p.right = p.parent = null;
            }
            //推断是不是须要举行红黑树构造调解
            TreeNode<K,V> r = p.red ? root : balanceDeletion(root, replacement);

            if (replacement == p) {  // detach
                TreeNode<K,V> pp = p.parent;
                p.parent = null;
                if (pp != null) {
                    if (p == pp.left)
                        pp.left = null;
                    else if (p == pp.right)
                        pp.right = null;
                }
            }
            if (movable)
                moveRootToFront(tab, r);
}

jdk1.8的删除逻辑完成比较庞杂,比拟jdk1.7而言,多了红黑树节点删除和调解:

  • 1、默许推断链表第一个元素是不是是要删除的元素;
  • 2、假如第一个不是,就继承推断当前争执链表是不是是红黑树,假如是,就进入红黑树内里去找;
  • 3、假如当前争执链表不是红黑树,就直接在链表中轮回推断,直到找到为止;
  • 4、将找到的节点,删撤除,假如是红黑树构造,会举行色彩转换、左旋、右旋调解,直到满足红黑树特征为止;

四、总结

1、假如key是一个对象,记得在对象实体类内里,要重写equals和hashCode要领,不然在查询的时刻,没法经由历程对象key来猎取参数值!
2、比拟JDK1.7,JDK1.8引入红黑树设想,当链表长度大于8的时刻,链表会转化为红黑树构造,发作争执的链表假如很长,红黑树的完成很大水平优化了HashMap的机能,使查询效力比JDK1.7要快一倍!
3、关于大数组的状况,能够提早给Map初始化一个容量,防止在插进去的时刻,频仍的扩容,因为扩容自身就比较斲丧机能!

五、参考

1、JDK1.7&JDK1.8 源码

2、美团手艺团队 - Java 8系列之从新认识HashMap

3、简书 - JDK1.8红黑树完成剖析-此鱼不得水

4、简书 - JJDK 1.8 中 HashMap 扩容

5、Java HashMap 基本口试常见问题

作者:炸鸡可乐
出处:www.pzblog.cn

腾讯云双十一活动