JDK8 内HashMap底层实现

构造

首先来看下hashMap的构造方法

    HashMap hashMap = new HashMap();

hashMap的无参构造方法非常简单,内部只是默认值的初始化,加载因子 0.75f

    public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }

直接来看下hashMap的两个参数构造方法,在默认值初始化的时候,tableSizeFor控制着hashMap size的大小,具体的实现下看:

    public HashMap(int initialCapacity, float loadFactor) {
        ... // 一些校验判断
        this.loadFactor = loadFactor;
        this.threshold = tableSizeFor(initialCapacity);
    }

可读性比较差,我们通过枚举几个cap值,来看下这个方法的作用:

    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;
    }

通过图示枚举的cap值,能够发现tableSizeFor 的作用,是为了保证hashMap size 是2的幂,例如我们new HashMap(7),底层实际分配的size 不是 7,而是23 = 8。为何会如此,看下文。
在这里插入图片描述

put

再来看下hashMap的put方法,当然key和value可Object类型的

    hashMap.put("key", "value");

首先底层会通过key 来获取一个hash 值,这里的hash可不仅仅是key.hashCode(),还需要右移16位并做亦或运算。之所以要进行右移,是为了在获取下标 tab[i = (n - 1) & hash] 时,让hashCode的高位也参与到运算中,防止结果频繁发生碰撞。而 (n - 1) & hash 为何能当下标呢,它的取值范围是(0, n -1)吗?不一定,这就需要对n进行限制,就是上面说的必须是2的幂数。原理也不难,拿32位原码试下就行了,2n -1 在 n (不包含)分割的低位上都是1,高位都是0,再进行&运算时,低位1才有用,而低位的组合结果范围就是(0, n - 1)

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

接下来底层开始调用putVal方法,hashMap的扩容逻辑都在这里。当首次oldTable 数组 null 或 容量 0 时,size会被初始化DEFAULT_INITIAL_CAPACITY = 16;当 oldTable 不为null,size 会被直接扩容 oldTable.length * 2 ,为何size 容量要扩容2 倍呢?看下文

    final Node<K,V>[] resize() {
        ...
            // table != null,更新 size 和 threshold
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; // threshold * 2
	    ...
        else { // zero initial threshold signifies using defaults
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        ...
        // 更新threshold = 16 * 0.75f
        threshold = newThr;
        Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    }

在oldTable不为null的情况:

当oldTab数组index上不存在碰撞,即没有链表,我们直接将oldTab[j] 随机放在newTab内,其下标位置取值范围e.hash & (newCap - 1),即(0, newCap - 1)

当有链表时,当随机的下标在index == 0碰撞,则将碰撞的元素插入到新链表尾部,当e.next null 时,将新链表移动到newTab[ j ] 位置上(注意这里的 j 其实就在原始的位置 j 上);当随机的下标未碰撞时,同样将链表内元素插入到一个新链表尾部,直到e.next null 时,将新链表移动到 newTab[j + oldCap] 位置上。而 j + oldCap 的取值范围(0, 2 * oldCap)。

            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 {
                            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;
                        }
                    }
              

上面的代码需要注意下TreeNode,这也是跟JDK7不一样的地方,在JDK8内引入了红黑树,在这里会判断当前的oldTab[j] 是否已经从链表转换成了红黑树了,如果是,则进行split操作。split的代码就不贴了,在内部有个方法需要注意下,当红黑树元素个数 < UNTREEIFY_THRESHOLD 时,即 lc < 6 时,又会将红黑树转换成链表。毕竟链表在插入操作上非常有优势。

                    else if (e instanceof TreeNode)
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);

再得到当前的table后,接下来会随机获取存储的index: (n - 1) & hash ,范围(0,n -1),如果当前index无元素存储,则直接创建一个节点Node对象,放在该index下。所以在底层,虽然我们put进的是key与value,但实际保存的是Node对象。

        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);

如果 (n - 1) & hash 获取的index发生了碰撞。首先会判断:当前传入的key,是否已经存在,如果存在,之后e.value = value引用地址替换,则将oldValue返给用户。注意,通过(n - 1) & hash 来获取下标,如果hash值相同,那么就一定会发生碰撞,但是hash值相同,不一定value值相同,可能在当前数组index下的是一个链表,具有多个元素。

下看判断,当前如果是红黑数了,即 p instanceof TreeNod ,则直接会putTreeVal操作。

再下看判断:循环当前table[index]下的链表,如果节点e是链表尾部了,那么直接将当前put的value所构造的Node插到链表尾部,而在遍历过程中如果发现有相同的key存在了,break循环,引用地址替换,将oldValue返给用户。

        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;
                }
            }
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }

在上代码内,需要注意下treeifyBin 方法,即当前链表的元素数量大于8时,需要做链表转换成红黑树的判断与操作。往下看

                       if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);

在这里能看到,转换操作也是有条件的,当我们tab.length >= 64 时,我们才会进行数的转换,否则我们会进行扩容处理。因为扩容后,能增加整个tab的散列性,也能有效避免链表过长的问题,而当大于64时,出于内存的考量,转换成红黑数,能增加查询效率。

    final void treeifyBin(Node<K,V>[] tab, int hash) {
        int n, index; Node<K,V> e;
        if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
            resize();
        else if ((e = tab[index = (n - 1) & hash]) != null) {
            ... // 转换
        }
    }
get

hashMap的get操作,相比来说就简单多了。首先我们会拿到key的hash。

如果tab length > 0 并 当前下标有值存在,当tab[(n - 1) & hash] 下标值即key值时,直接返回该value,否则判断当前结构是否为红黑数,如果是则执行getTreeNode 获取对于value,如果当前结构为链表,则遍历查找相同key ,返回对应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))))
                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;
    }
原创文章 62 获赞 208 访问量 17万+

猜你喜欢

转载自blog.csdn.net/MingJieZuo/article/details/105801688