HashMap 1.8

一、HashMap的结构:数组+链表

1、那么数组在哪里?有多大?

我们来到HashMap的源码,可以发现它里面有个数组  transient Node<K,V>[] table;

数组的初始大小为16,static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;

数组最大为2^30,static final int MAXIMUM_CAPACITY = 1 << 30;

 

2、数组已经明确了,那么链表呢?它的每个节点应该是怎么样的?

  我们先自己想一想,应该有个key,有个value,有个next。再来看看源码中的实现,通过上面的数组,我们可以知道数组里面的每一个元素都是Node<K,V>,我们点进去,看看它的实现。

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

key,value,next都有了,很符合我们的预期。那么这个hash是用来干嘛的?是用来定位的,我要插入一个Node的话,它应该在数组的哪一个位置。

 

二、HashMap的插入

我们先想一想,把一个节点插入HashMap中,需要考虑些什么呢?

1)既然是数组+链表的结构,那么我插入的时候,数组有没有初始化呢?

2)定位。我这个节点应该放在数组的哪个位置上?如果这个位置上已经有元素了,那么跟在后面形成链表?

3)链表太长了,插入和查找的效率都很低,怎么办?

 

1、我们找到它的插入方法,put

public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }

我们先忽略hash(key)方法,先看putVal()方法

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;
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
    ...
}

我们看到n = (tab = resize()).length这一行,继续探索resize()方法。此方法中有很多if-else,此次我们只看数组初始化时,即table == 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;
		
        newCap = DEFAULT_INITIAL_CAPACITY; //初始容量
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); //数组使用了多少,才开始扩容
       
        threshold = newThr;//threshold是实例变量,用来记录扩容阈值
        @SuppressWarnings({"rawtypes","unchecked"})
            Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; //初始化数组
        table = newTab; 
        return newTab;
    }

可以看到,正是在resize()方法里面,初始化了数组。解决了我们第一个问题,数组什么时候初始化

 

2、resize()看了,我们再回到put方法

public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }

再来看看hash(key)这个方法

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

我们来解释一下 (h = key.hashCode()) ^ (h >>> 16) 这句话

  key.hashCode() 调用的是Object的hash算法,返回一个32位的数字。此时h有32位

  h >>> 16,h向右移动16位

 (h = key.hashCode()) ^ (h >>> 16) 将它们进行异或操作。作用是充分让h的每一位都参与进来,让Node节点尽可能地定位在数组的不同位置上

这句话的作用还是不理解?没关系,我们下面讲如何定位,还会提到(h = key.hashCode()) ^ (h >>> 16)的作用

 

3、我们回到putVal()方法的定位操作上

  如果让我们自己想的话,我们可以会用 hash % (n-1) 来定位,而 %操作,并没有&操作来得高效。那让我们来解释一下,这个&操作吧

  此时hash值,是我们上面看到的(h = key.hashCode()) ^ (h >>> 16),是个32位的数

 n - 1 是15 (由于我们是第一次初始化,所以取的是初始默认容量,n=16)

  可以看到hash的值只有最后的几位参与了运算,那多个不同的hash,只要最后几位相同,他们的位置不就重复了吗?数组的空间就不能充分利用了。

  这也就是,为什么我们之前进行(h = key.hashCode()) ^ (h >>> 16)操作的原因了,让它的每一位都参与进来,让他们尽可能地定位在数组的不同位置上。

  为了维持n-1的值,是1111, 11111, 111111这种形式,HashMap的容量规定是2的倍数

 

4、 如何定位我们已经知晓了,那这个位置上已经存在元素的话,hashMap将会做什么操作呢?

当数组的这个位置上已经存在元素的时候,它会在后面形成链表。当链表太长的时候,它会转化成红黑树。

static final int TREEIFY_THRESHOLD = 8; 这就是转化成红黑树的阈值,当链表的节点数量达到8的时候,进行转化

static final int UNTREEIFY_THRESHOLD = 6; 当红黑树的节点数量达到6的时候,又转回链表

 

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        
            Node<K,V> e; K k;
			//1、数组上,如果hash值相等,key也相等,把这个位置的旧元素记下来,方便下面的新值取代旧值
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            else if (p instanceof TreeNode)
				// 2、如果是红黑树结构,那么作为树的节点,进行插入
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
				// 3、如果是链表结构,那么跟在链表的末尾
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        if (binCount >= TREEIFY_THRESHOLD - 1) 
						// 如果超过树化阈值,那么将链表转成树
                            treeifyBin(tab, hash);
                        break;
                    }
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
						//链表上,如果hash值相等,key也相等,把这个位置的旧元素记下来,方便下面的新值取代旧值
                        break;
                    p = e;
                }
            }
            if (e != null) { // 是否hash相等,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;
    }

 

5、我们已经知晓,当hash碰撞的时候,hashMap会形成链表或红黑树。那我们再来看看resize()方法,这次我们关注它的扩容操作,具体操作如下:

0)容量变为原来的两倍

1)新数组的创建

2)遍历原来的数组,将存在的元素,移到新的数组

 2.1)如果是单个元素的话,那么hash值 & 新容量-1,定位到新数组的位置上

 2.2)如果是红黑树的话,那么打散节点

2.3)如果是链表的话,根据hash值的不同,把链表拆成两个链表

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
        }
        threshold = newThr;
        @SuppressWarnings({"rawtypes","unchecked"})
			//1、新容量数组,创建
            Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
        table = newTab;
        if (oldTab != null) {
			//2、遍历原来的数组,将存在的元素移位到新数组
            for (int j = 0; j < oldCap; ++j) {
                Node<K,V> e;
				//如果数组的这个位置上有元素,那么进行移位操作
                if ((e = oldTab[j]) != null) {
                    oldTab[j] = null;
                    if (e.next == null)
						//3、如果只有单个元素,不是链表和红黑树,那么定位(hash值 & 新容量-1)
                        newTab[e.hash & (newCap - 1)] = e;
                    else if (e instanceof TreeNode)
						//4、如果是红黑树的话,那么打散节点
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    else { //5、是链表的话,根据hash值的不同,把链表拆成两个链表
                        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;
    }

6、待续

 1)红黑树的结构

2)链表如何转红黑树

3)红黑树如何转链表

4)扩容的时候,红黑树如何打散

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章