本文基于JDK1.8来分析
hashMap详解
- 构造方法
//指定初始化的容量 不过new的过程并不会立即初始化
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
//指定初始化容量 以及加载因子 主要用来计算扩容时机
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
this.threshold = tableSizeFor(initialCapacity);
}
//无参构造
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
//传入map
public HashMap(Map<? extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
}
-
put
public V put(K key, V value) { //具体调用putVal 同时对key进行hash计算 return putVal(hash(key), key, value, false, true); } static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); } //举例来说明下 hash过程 // 传入的 key为 111 // 111的hashcode为48657 二进制 表示为 1011111000010001 // 0000 0000 0000 0000 1011 1110 0001 0001 >>> 16 = 0000 0000 0000 0000 0000 0000 0000 0000 // 0000 0000 0000 0000 1011 1110 0001 0001 ^ 0000 0000 0000 0000 0000 0000 0000 0000 = // 0000 0000 0000 0000 1011 1110 0001 0001 // 0000 0000 0000 0000 1011 1110 0001 0001 转化为 10进制为48657 -
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) //第一次put内容的时候 条件成立 肯定会进来 //初始化table n = (tab = resize()).length; if ((p = tab[i = (n - 1) & hash]) == null) //计算索引对应的数组位置是否为null 为空的话 直接new一个node 放入对应的索引位置即可 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)))) //当前索引下的node节点的hash值 等于 传入的hash值 同时 node节点的key等于输入的key //将p赋值给e e = p; else if (p instanceof TreeNode) //当前索引对应的node为树节点 以树节点方式插入节点 e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); else { //当前索引位置下的node节点 存在链表 for (int binCount = 0; ; ++binCount) { if ((e = p.next) == null) { //循环直到 当前索引位置的node的链表的最后一个 //创建新的node节点并赋值给当前node下的链表的 最后一个节点的下一个 p.next = newNode(hash, key, value, null); if (binCount >= TREEIFY_THRESHOLD - 1) //根据binCount 是否大于 8-1 来决定是否需要转化为红黑树 treeifyBin(tab, hash); //放入成功则退出循环 break; } if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) // //当前node节点的hash值 等于传入的hash值 同时 node节点的key等于输入的key break; //将e 节点赋值给p节点 p = e; } } if (e != null) { //e节点不为空说明 存在和现在插入相同的节点 //获取到e节点的 value V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) //传入onlyIfAbsent为false 或者oldValue为空 //进行覆盖旧的值 e.value = value; //数据访问后回调 afterNodeAccess(e); //返回旧的值 return oldValue; } } //计算修改数量 ++modCount; if (++size > threshold) //++集合的大小 大于threshould //进行扩容 resize(); afterNodeInsertion(evict); return null; } -
resize
final Node<K,V>[] resize() { Node<K,V>[] oldTab = table; //初始化逻辑进入时oldCap为0 int oldCap = (oldTab == null) ? 0 : oldTab.length; //这里不同的构造又有不用的计算方式 //使用无参数构造 new HashMap<>(); oldThr = 0 //使用带初始化容量构造或者初始化容量和加载因子构造 threshold = tableSizeFor(初始化容量)= 将输入的容量向上转化为 2的N次 幂 输入为9 转化的后的值为 16 稍后在具体看这个方法 // int oldThr = threshold; int newCap, newThr = 0; if (oldCap > 0) { //旧数组容量大于0 if (oldCap >= MAXIMUM_CAPACITY) { //旧数组容量大于等于最大容量 //threshold 为integer最大值 threshold = Integer.MAX_VALUE; return oldTab; } else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) //新容量= 原容量*2 小于最大容量 并且 旧容量大于等默认初始化容量 //则新的threshold 等于原threshold*2 newThr = oldThr << 1; // double threshold } else if (oldThr > 0) //使用带初始化容量构造或者初始化容量和加载因子构造 newCap = threshold newCap = oldThr; else { //使用无参构造时 newCap= 默认值 也就是16 newCap = DEFAULT_INITIAL_CAPACITY; //newThr = 16*0.75 = 12 newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); } if (newThr == 0) { //newThreshold=0 时需要计算 float ft = (float)newCap * loadFactor; newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE); } //将newThr 也就是 容量 * 加载因子 赋值给 threshold threshold = newThr; //初始化新容量的Node数组 @SuppressWarnings({"rawtypes","unchecked"}) Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; //同时将newTab 赋值给table变量 table = newTab; if (oldTab != null) { //初始化时oldTab 肯定为空的 不满足条件 //容量不够 进入到 扩容阶段 for (int j = 0; j < oldCap; ++j) { //循环旧的node数组 Node<K,V> e; if ((e = oldTab[j]) != null) { //获取每个索引下标的元素 //将旧的数组索引下标对应的元素 清空 oldTab[j] = null; if (e.next == null) //node 元素的下一个节点为空 也就是不存在 hash冲突 没有形成链表 或者 树结构 //重新计算旧的元素在新数组的索引位置 并放入 newTab[e.hash & (newCap - 1)] = e; else if (e instanceof TreeNode) //如果为树的结构 ((TreeNode<K,V>)e).split(this, newTab, j, oldCap); else { // preserve order //node节点下存在链表 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) { //节点的hashcode 和旧容量进行& //为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; } -
putTreeVal 放入树节点
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;;) { int dir, ph; K pk; if ((ph = p.hash) > h) dir = -1; else if (ph < h) dir = 1; else if ((pk = p.key) == k || (k != null && k.equals(pk))) return p; else if ((kc == null && (kc = comparableClassFor(k)) == null) || (dir = compareComparables(kc, k, pk)) == 0) { if (!searched) { TreeNode<K,V> q, ch; searched = true; 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); } TreeNode<K,V> xp = p; 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; xp.next = x; x.parent = x.prev = xp; if (xpn != null) ((TreeNode<K,V>)xpn).prev = x; moveRootToFront(tab, balanceInsertion(root, x)); return null; } } } -
treeifyBin
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) //如果tab为空或者 table的长度小于 64 则进行扩容 resize(); else if ((e = tab[index = (n - 1) & hash]) != null) { //获取对应数组索引下的node节点 TreeNode<K,V> hd = null, tl = null; do { //将node替换为TreeNode TreeNode<K,V> p = replacementTreeNode(e, null); if (tl == null) hd = p; else { p.prev = tl; tl.next = p; } tl = p; } while ((e = e.next) != null); if ((tab[index] = hd) != null) hd.treeify(tab); } } 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); }
重点说一下几个地方
-
计算hash的过程
static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }这里使用key的hashcode 和 key的hashcode 右移16位 进行异或运算
最后获取元素应该保存的位置索引时 又进行 (hash(key)&(table.length-1) ) 来获取元素索引位置 为什么不直接使用元素的hashcode 和 (table.length-1) 做与运算 而是又是异或运算 又是右移运算的 个人理解 当数组容量小的时候 计算元素在数组中的位置
hash&(table.length-1) 这样只用到了hash的低位 当不同的hash 低位相同 高位不同时会产生冲突 现在hash值将hashcode低16位与高16位进行异或(相同为0 不同为1)这样相当于混合了高低位 增加了随机性 减少了hash冲突 元素的分布更加随机
-
扩容机制
扩容出现的场景
- 第一次存入数据 因为hashmap使用的为延迟初始化 即在放入元素的时候在进行判断
- 存入元素后 将数组长度加+1 作为新的数组长度 如果 大于threshold 大于则进行扩容
-
当node节点下存在链表的扩容
{ // preserve order //node节点下存在链表 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) {//注释4 //节点的hashcode 和旧容量进行&运算 //构建为索引不变的链表的头 if (loTail == null) //注释5 loHead = e; //注释7 else //注释6 loTail.next = e; // loTail = e; } else { //构建为索引改变的链表头 新的索引 = 原先索引+ 旧table的容量 if (hiTail == null) hiHead = e; else hiTail.next = e; hiTail = e; } } while ((e = next) != null); if (loTail != null) { //注释8 //索引不变的链表的尾结点不为空 //索引不变的链表的尾结点赋值为空 loTail.next = null; //同时将新数组的 当前索引位置 赋值为索引不变的链表的头 newTab[j] = loHead; } if (hiTail != null) { //索引改变的链表的尾结点不为空 //索引改变的链表的尾结点下一个为空 hiTail.next = null; //同时将 索引改变后的链表头结点 放到新的table 新的索引位置上 //新索引的位置 = 原先索引位置 + 旧table的容量 newTab[j + oldCap] = hiHead; } }正常情况下,计算节点在table中的下标的方法是:hash&(oldTable.length-1),扩容之后,table长度翻倍,计算table下标的方法是hash&(newTable.length-1),也就是hash&(oldTable.length*2-1),于是我们有了这样的结论:这新旧两次计算下标的结果,要不然就相同,要不然就是新下标等于旧下标加上旧数组的长度。
hash值的每个二进制位用abcde来表示,那么,hash和新旧table按位与的结果,最后4位显然是相同的,唯一可能出现的区别就在第5位,也就是hash值的b所在的那一位,如果b所在的那一位是0,那么新table按位与的结果和旧table的结果就相同,反之如果b所在的那一位是1,则新table按位与的结果就比旧table的结果多了10000(二进制),而这个二进制10000就是旧table的长度16。
换言之,hash值的新散列下标是不是需要加上旧table长度,只需要看看hash值第5位是不是1就行了,位运算的方法就是hash值和10000(也就是旧table长度)来按位与,其结果只可能是10000或者00000。
所以,注释4处的e.hash & oldCap,就是用于计算位置b到底是0还是1用的,只要其结果是0,则新散列下标就等于原散列下标,否则新散列坐标要在原散列坐标的基础上加上原table长度。
理解了上面的原理,这里的代码就好理解了,代码中定义的四个变量:
loHead,下标不变情况下的链表头
loTail,下标不变情况下的链表尾
hiHead,下标改变情况下的链表头
hiTail,下标改变情况下的链表尾
而注释4处的(e.hash & oldCap) == 0,就是代表散列下标不变的情况,这种情况下代码只使用了loHead和loTail两个参数,由他们组成了一个链表,否则将使用hiHead和hiTail参数。
其实e.hash & oldCap等于0和不等于0后的逻辑完全相同,只是用的变量不一样。
以等于0的情况为例,处理一个3–>5–>7的链表,过程如下:
首先处理节点3,e3,e.next5
1,注释5,一开始loTail是null,所以把3赋值给loHead。
2,注释7,把3赋值给loTail。
然后处理节点5,e5,e.next7
1,注释6,loTail有值,把e赋值给loTail.next,也就是3.next==5。
2,注释7,把5赋值给loTail。
现在新链表是3–>5,然后处理节点7,处理完之后,链表的顺序是3–>5–>7,loHead是3,loTail是7。可以看到,链表中节点顺序和原链表相同,不再是JDK1.7的倒序了。
代码到注释8这里就好理解了,
只要loTail不是null,说明链表中的元素在新table中的下标没变,所以新table的对应下标中放的是loHead,另外把loTail的next设为null
反之,hiTail不是null,说明链表中的元素在新table中的下标,应该是原下标加原table长度,新table对应下标处放的是hiHead,另外把hiTail的next设为null。