3.4 HashMap扩容方法
目标:图解+代码(map扩容与数据迁移)
注意:扩容复杂、绕、难
图解前提: 按8和16的长度讲(图片不容易展示)
迁移前:长度8 扩容临界点6(8*0.75)
迁移过程
核心源码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 &&//扩容2倍:oldCap << 1,左移1位,相当于oldCap乘以2的1次方 oldCap >= DEFAULT_INITIAL_CAPACITY) newThr = oldThr << 1; // 扩容2倍:将阈值threshold*2得到新的阈值 } else if (oldThr > 0) // HashMap(int initialCapacity, float loadFactor)调 用 newCap = oldThr; else { // zero initial threshold signifies using defaults newCap = DEFAULT_INITIAL_CAPACITY;//第一次 put!!!!!!!!!!!!!!!!!! newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);//扩充 阈值 } if (newThr == 0) {//如果新阈值为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;//gc处理 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;//低位链表(原位置i) Node<K,V> hiHead = null, hiTail = null;//高位链表(i+n位 置) Node<K,V> next; do { next = e.next; if ((e.hash & oldCap) == 0) {// 如果为0,元素位置在扩容后 数组中的位置没有发生改变(低位) if (loTail == null) loHead = e;// 头节点 else loTail.next = e; loTail = e; } else {//不为0,元素位置在扩容后数组中的位置发生了改变,新的下 标位置是(原下标位置+原数组长) 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;//返回新数组 }
总结(扩容与迁移):
1、扩容就是将旧表的数据迁移到新表
2、迁移过去的值需要重新计算hashCode,也就是他的存储位置
3、关于位置可以这样理解:比如旧表的长度8、新表长度16
旧表位置4有6个数据,假如前三个hashCode是一样的,后面的三个hashCode是一样的
迁移的时候;就需要计算这6个值的存储位置
4、如何计算位置?采用低位链表和高位链表;如果位置4下面的数据e.hash & oldCap等于0,
那么它对应的就是低位链表,也就是数据位置不变
e.hash & oldCap不等于0呢?就要重写计算他的位置也就是j + oldCap(4+8);这个12,就是高位链表
位置(新数组12位置)
3.5 HashMap获取方法
目标:图解+断点分析get源码
获取流程
get主方法
public V get(Object key) { Node<K,V> e; return (e = getNode(hash(key), key)) == null ? null : e.value;//先计算 hashcode }
getNode方法
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 && // 数组查找!!!!! ((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; }
总结:
查询思路比较简单,如果是数组直接返回、如果是红黑实例,就去树上去找,最后,去做链表循环查找
3.6 HashMap移除方法
目标:图解+断点分析remove源码
移除流程
tips:
两个移除方法,参数上的区别
走的同一个代码
移除方法:一个参数
public V remove(Object key) { Node<K,V> e; 定义一个节点变量,用来存储要被删除的节点(键值对 return (e = removeNode(hash(key), key, null, false, true)) == null ? null : e.value; }
移除方法:二个参数
@Override public boolean remove(Object key, Object value) { return removeNode(hash(key), key, value, true, true) != null; }
核心方法removeNode
/** * Implements Map.remove and related methods * * @param hash 扰动后的hash值 * @param key 要删除的键值对的key,要删除的键值对的value,该值是否作为删除的条件取决于 matchValue是否为true * @param value key对应的值 * @param matchValue 为true,则当key对应的值和equals(value)为true时才删除;否则不关 心value的值 * @param movable 删除后是否移动节点,如果为false,则不移动 * @return 返回被删除的节点对象,如果没有删除任何节点则返回null */ final Node<K,V> removeNode(int hash, Object key, Object value, boolean matchValue, boolean movable) { Node<K,V>[] tab; Node<K,V> p; int n, index; if ((tab = table) != null && (n = tab.length) > 0 && (p = tab[index = (n - 1) & hash]) != null) { Node<K,V> node = null, e; K k; V v; if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) node = p;//寻址后的值给node else if ((e = p.next) != null) { if (p instanceof TreeNode) node = ((TreeNode<K,V>)p).getTreeNode(hash, key);//红黑查找 else { do { //链表查找 if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) { node = e; break; } p = e;// 走到这里,说明e也没有匹配上,P-->E;表示p是e的父节点 } while ((e = e.next) != null);//如果e存在下一个节点,那么继续去匹 配下一个节点 } }//如果node不为空,说明根据key匹配到了要删除的节点 if (node != null && (!matchValue || (v = node.value) == value || (value != null && value.equals(v)))) { if (node instanceof TreeNode) ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);//红 黑删除 else if (node == p)// 如果该节点不是TreeNode对象,node == p 的意思是该 node节点就是首节点 tab[index] = node.next;//由于删除的是首节点,那么直接将节点数组对应 位置指向到第二个节点即可 else p.next = node.next; 如果node节点不是首节点,此时p是node的父节 点,由于要删除node,所有只需要把p的下一个节点指向到node的下一个节点即可把node从链表中删除了 ++modCount;//HashMap的修改次数递增 --size;// HashMap的元素个数递减 afterNodeRemoval(node);//子类扩展 return node;//返回删除后的节点 } } return null;//找不到删除的node,返回null }
总结:
移除和查询路线差不多,找到后直接remove
注意他的返回值,是删除的那个节点的值
4 哈希最常见面试题
4.1 为什么要从JDK1.8之前的链表设计,修改为链表或红黑树的设计?
当某个链表比较长的时候,查找效率还是会降低。
如果table[index]的链表的节点的个数比较少,(8个或以内),就保持链表。如果超过8个,那么
就要考虑把链表转为一棵红黑树。
4.2 什么时候树化?
table[index]下的结点数一达到8个就树化吗?
如果table[index]的节点数量已经达到8个了,还要判断table.length是否达到64,如果没有达到
64,先扩容
4.3 什么时候从树–>链表
当你删除结点时,这棵树的结点个数少于6个,会反树化,变为链表
4.4 初始容量是16,为什么是2的指数次幂?
1、方便计算
2、扩容与数据迁移(重点)
因为2的幂次方在二进制的表达中可以保证后几位均为1,为什么要这样做呢?举一个例子就很清楚了,
基于按位与操作的思想,两数都为1才为一,若数组长度为15,(15-1)即14表示的话就是1110,
1110&1110和1110&1111的结果是相同的,这不就增加了哈希碰撞的概率了,因此,保持n-1的后几位
为1,就可以根据hash值的后几位来判别应该放在哪个桶里了
4.5 为什么加载因子是0.75呢?
其实是通过泊松分布来计算的
选择0.75作为默认的加载因子,完全是时间和空间成本上寻求的一种折衷选择
总结:
通过统计学和概率计算得出来的结果
4.6 什么是哈希冲突(碰撞)?,如何解决
如果两个不同的元素(key、value、hash不同),通过哈希函数得出的实际存储地址相同
也就是说,当我们对某个元素进行哈希运算,得到一个存储地址,然后要进行插入的时候,发现已经被
其他元素占用了,其实这就是所谓的哈希冲突,也叫哈希碰撞
该如何解决呢?链表
4.7 获取hash 值,为什么不用hashCode直接取呢?左移、右移和异或怎么计算?
让hash分布、散列更加均匀,减少hash碰撞的概率。
tips:(h = key.hashCode()) ^ (h >>> 16)
是如何计算的?
异或:右移
举例:
key.hashCode() int转32二进制为0000010101001010 0101001011110001
右移16位,高位不足进行补零(直接在前面加16个0)
0000010101001010 0101001011110001(移动前)
0000000000000000 0101001011110001 (移动后)
开始异或(相同为0.不同为1)
0000010101001010 0101001011110001
0000000000000000 0101001011110001
异或后的(相同为0,不同为1)
0000010101001010 0000000000000000
关于左移
2^0:在二进制中表示1
注意:上面前导为1,计算出来的就是负数
关于右移
tips
以上没有按照实际32位作图,只是举例,图片放不下
4.8 JDK1.8相比1.7主要优化的地方?
jdk1.8是链表+数组+红黑树;
jdk1.7链表采用头插法,头插法比较快,但容易造成死循环;jdk1.8是尾插法;
5 HashMap常问面试题
HashMap常问面试题