HashMap|put方法解析
1 2 3 4 5 6
| public class TestHashMap { public static void main(String[] args) { HashMap<Object, Object> hashMap = new HashMap<>(); hashMap.put("a", "a"); } }
|
直接点进put方法内,看到实则调用putVal方法
1 2 3
| public V put(K key, V value) { return putVal(hash(key), key, value, false, true); }
|
putVal方法
思考
就算不看源码之前,自己想一下该怎么往哈希桶中插入一些值(首先肯定要了解HashMap的基本结构的啊!):
- 先根据一些算法找到你要插入元素在哈希桶中的位置。
- 找到这个位置之后判断一下这个位置是否为空,为空则直接插入,执行完毕。
- 如果不为空则,看一下你要插入的是否和这个元素的key相等,相等则替换值即可。
- 否则肯定是判断这个结点的下一个是否为空,为空的话,说明这个哈希桶中不是链表就是红黑树。
- 先判断一下它是否为树,如果为树,按照红黑树的方式插入元素。
- 否则为链表则遍历该链表啊,看是不是和你的值冲突,如果冲突就替换,否则在链表最后插入即可。
这个过程很简单,实际上除了TreeNode类中有关的那些方法,其它的实现都相对简单。那个TreeNode要对红黑树特别了解才能理解明白。上述的几个步骤中就是其中缺少了对于扩容的一些处理,对于链表每次要是插入一个元素就应该考虑是否扩容,是否树化。所以在第6步骤插入元素后需要考虑这些。(替换则不需要考虑了啊)
源码中的实现大概思想是这样,但存在许多细节。这里随便列举几个:
首先这个一个元素到底存放在哈希桶中哪个位置,这个算法具体怎么实现的??
如果达到扩容阈值,扩容之后那哈希桶中的元素该怎么重新分配(这个是在resize扩容方法中处理的)???
还可以得出一个问题就是为什么HashMap的长度要是2的幂次方(这个和1和2问题有关)?
…
后面都会详细解释这些问题、

源码解读
看putVal方法之前要明白一个元素是怎么确定在table数组中的位置的。
(n- 1) & hash
n为table的长度,hash为可以的hash并且这个hash并不是直接取key的哈希而是经过hash这个扰动函数让hash更加分散
1 2 3 4
| static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }
|
(n- 1) & hash到底是什么意思?
这个含义是hash % n的意思。
首先hashMap的长度规定为2的次幂,那么长度的2进制必然为前面一个1后面全是0,比如8是1000,4是0100。
那么这个2进制减去1是多少,比如8的二进制-1 :1000 - 1 = 0111 原来为1的位变0后面全部为1。
然后和hash进行与操作,&操作只有在全是1的情况下才为1,可以看出来了吧,这个n-1&hash 无论hash的高位多么的花里胡哨的全给我变成0,只保留后面的几位,如果hash>n-1 的话就是求余操作,否则就是hash这个值。
这你也就明白了HashMap的长度为什么是2的次幂了吧。这个在resize方法中也有体现后续会说明。
参数含义
- key的哈希值
- key
- value
- 是否覆盖原来旧的值,就是HashMap中存在与你这个key相同的,false覆盖,true不覆盖
- evict,如果为false,则处于创建模式(无特殊含义,在HashMap的一个构造函数中传过来的是false)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
| 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); else { 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) treeifyBin(tab, hash); break; } if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; } } if (e != null) { V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); return oldValue; } } ++modCount; if (++size > threshold) resize(); afterNodeInsertion(evict); return null; }
|
确定元素在哈希桶中的位置:
小结
这个过程和自己想的过程一样吗,就是它插入时利用了一个中间变量e来记录与我key值相同的结点,如果这个e不为空则说明要覆盖这个结点,然后根据onlyIfAbsent这个参数,注意这个若这个e的value为null也进行覆盖!!覆盖之后就返回原来旧的value。
如果e为空,则判断是否需要扩容了,然后返回null。
resize方法
思考
不看源码之前自己现想一下让你实现扩容怎么实现(这个比较难想得出来!!):
- 简单的思考一下,既然是扩容方法,肯定是要确定一下新的table容量,扩容阈值threshold肯定也需要重新设定一下子啊,不设定这个容量>扩容阈值不得每次都扩容吗。可得出第一个步骤就是求出新的table容量和相应的扩容阈值threshold。具体新的table是原来容量的多少倍肯定看源码才明白,现在明白新的table长度肯定是原来的2的幂次方的倍数。!
- 第1步中既然得到了扩容阈值和新容量,现在肯定要new一个新的table出来,然后把原来的table拷贝到这个新的table中。再细化一下,肯定是遍历原来的table然后判断哈希桶的根中是否存在数据存在数据则说明不是链表就是树,分别对链表和树进行相关处理,如果为空则跳过判断下一个了。
- 第2步拷贝到新的table完成后自然就返回这个新的table即可!!
如果只知道resize看表面来说是扩容方法,但是它还有一层含义就是初始化HashMap这个职责。这个我是肯定想不到,设计者思考的。
这个第2个步骤中对链表的处理,你想一下该怎么处理,现在的table是原来的2倍,链表该怎么移动??后续给出答案!
源码解读
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73
| 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; } else if (oldThr > 0) newCap = oldThr; else { 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; 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 { 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; }
|

统一说明这两个问题(这也和为什么长度要是2的次幂有关)
举个栗子!旧容量为4,扩容后为8:

小结
这个resize方法很重要,是兼顾了初始化,和扩容。这个方法大致分为2部分,前面一部分是确定新的table长度和新的扩容阈值threshold。后半部分就是new个新的table然后复制元素到新的table中然后返回新的table。