欢迎您的访问
专注于Java技术系列文章的Java技术分享网站
精选技术专栏

浅析JDK1.8中的HashMap

JetBrains全家桶破解:IDEA 2021 破解到 2099 年教程

Java集合类是面试中经常会问到的,也是我们学习Java的基础与重点,而HashMap是其中十分重要的部分,值得我们去认真钻研它的实现。下面将通过HashMap中的几个重要方法加深我们对Java集合类设计的理解。

在使用HashMap时,最常用的可能是它的get和put方法,那我们就先从这两个最常用的方法开始。

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;
    //判断tab是否为空,hash对应的tab数组上的元素是否为空
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (first = tab[(n - 1) & hash]) != null) {
        //首先判断tab[i]上的元素(即链表头)是不是要找的,如果hash相等、key相同(引用同一对象、equals判断为true),则找到这个元素
        if (first.hash == hash && // always check first node
            ((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值判断这个key对应的元素应该在数组table的哪个元素中,再在以该元素为头节点的链表或者红黑树中遍历寻找key相同的元素。

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;
    //判断tab是否为空,若为空则调用resize方法扩容
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    //找到hash值对应在数组中的位置的元素,若为空则构造新节点直接替代
    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);
        //遍历链表
        else {
            for (int binCount = 0; ; ++binCount) {      //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;
                }
                //如果找到key
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;
            }
        }
        //改变key对应节点的value
        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;
}

在看完get和put方法,我们发现每当需要根据hash值判断这个key应该去table数组的哪个下标寻找时,都会出现这样的计算方法:

first = tab[(n - 1) & hash]

其中n为table数组的长度,那么为什么要这样做呢?一种解释是因为hash值可能会大于数组长度,这么做其实是一个变相的“求余数”运算,即:

(n-1) & hash == hash % n;

因为HashMap中有一个方法:tableSizeFor,它的作用是根据给定的数组容量计算出一个合适的“二的整数次幂”的容量值。

/**
 * Returns a power of two size for the given target capacity.
 */
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;
}

所以我们知道HashMap中的table数组的长度是二的次幂,所以假设我们设置的长度为5,则:

cap = 5
n = cap - 1 =  4 = 0 1 0 0
n |= n >>> 1;    0 1 0 0 | 0 0 1 0 = 0 1 1 0 = 6
n |= n >>> 2;    0 1 1 0 | 0 0 0 1 = 0 1 1 1 = 7
n |= n >>> 4;    0 0 0 0 | 0 1 1 1 = 0 1 1 1 = 7
n |= n >>> 8;    0 0 0 0 | 0 1 1 1 = 0 1 1 1 = 7
n |= n >>> 16;   0 0 0 0 | 0 1 1 1 = 0 1 1 1 = 7
return n + 1     7 + 1 = 8 

参考:stackoverflow.com/questions/2…

由上可知我们的数组长度为8,由于长度都为二的次幂,即n=10…0,则n-1=01..1,n-1的二进制全为1,当hash值对(n-1)做与运算时,只能留下小于等于(n-1)的部分,所以就相当于hash对(n)求余数

所以,我们现在也清楚了为什么一定要用tableSizeFor方法确保我们的数组的长度是二次幂就是为了方便根据hash值计算key值在table数组中的坐标

在put方法中,我们可以看到resize方法的出现频率也比较高,resize方法是一个扩容方法,当table中的元素超过设定的阈值后,为了保证对HashMap操作的时间复杂度,需要对数组中的元素进行扩容操作。

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;
    //设置新容量大小
    if (oldCap > 0) {       //旧的容量大于零
        if (oldCap >= MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&   //新容量为旧容量的两倍
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
            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);
    }
    //设置新threshold
    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;     //table指向新数组
    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;      //重新计算它的位置,n变为newCap-1
                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;     //表示一条链表,该链表在新数组中的下标比原来增加oldCap
                    Node<K,V> next;
                    //遍历下标为j的那条链表,将原来的整条链表拆分成两个链表,一个是在新数组中位置不变的,另一个是在新数组中的位置是旧数组中的位置增加oldCap
                    do {
                        next = e.next;
                        //原数组中计算key在数组中的位置:hash&(oldCap-1),新数组为:hash&(2*oldCap-1)=hash&(oldCap<<1 - 1),所以如果hash对应在oldCap中的最高位为0,则hash&(oldCap-1)==hash&(2*oldCap-1),即在新数组中位置不变
                        if ((e.hash & oldCap) == 0) {       //代表那些hash值重新计算后位置不变的节点
                            if (loTail == null)
                                loHead = e;
                            else
                                loTail.next = e;
                            loTail = e;
                        }
                        else {      //代表那些hash值重新计算后位置增加oldCap的节点
                            if (hiTail == null)
                                hiHead = e;
                            else
                                hiTail.next = e;
                            hiTail = e;
                        }
                    } while ((e = next) != null);
                    //将新构建的链表分成两部分(位置不变、位置后移oldCap)插入到新数组中
                    if (loTail != null) {
                        loTail.next = null;
                        newTab[j] = loHead;
                    }
                    if (hiTail != null) {
                        hiTail.next = null;
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    return newTab;
}

由上所示,因为数组的长度是二的次幂,所以节点在新数组的位置有且仅有两种情况:1.与原数组中的位置相同;2.比原数组中的位置后移了一个oldCap。

原数组中计算key在数组中的位置:hash&(oldCap-1),新数组为:hash&(2oldCap-1)=hash&(oldCap<<1 – 1),所以如果hash对应在oldCap中的最高位为0,则hash&(oldCap-1)==hash&(2oldCap-1),即在新数组中位置不变

HashMap中的put死循环问题

酷壳网的博客:疫苗:Java HashMap的死循环

文章永久链接:https://tech.souyunku.com/?p=42579

赞(77) 打赏



版权归原创作者所有,任何形式转载请联系作者;搜云库技术团队 » 浅析JDK1.8中的HashMap

JetBrains全家桶破解:IDEA 2021 破解到 2099 年教程
JetBrains全家桶破解:IDEA 2021 破解到 2099 年教程

评论 抢沙发

一个专注于Java技术系列文章的技术分享网站

觉得文章有用就打赏一下文章作者

微信扫一扫打赏

微信扫一扫打赏