本篇不介绍红黑树,后续会单独分析。
- JDK 1.8
也没看过jdk1.7 所以不做对比分析了
需准备知识
- 异或运算
- 位移
- 知道HashMap的数据结构有哪些
- 知道单链表大致是怎么一回事
能学到什么
- HashMap如何计算扩容阙值
- 哈希表需要树化的标准条件
- 哈希表什么时候初始化
- 为什么需要扩容
- HashMap如何增加hash的散列性
- HashMap如何确定桶位
- 链表如何删除元素
这些问题都会在源码分析的注释中有体现出来。
可能有些没分析到,欢迎留言,后续继续补充。
文章目录
- HashMap核心属性
- HashMap构造方法
- 哈希表元素:Node
- 如何计算扩容阙值:
tableSizeFor 方法 - 如何加hash的散列性:
扰动函数hash() - 如何路由寻址(计算出桶位):`桶位 = (table.length-1)& hash1
- HashMap核心方法:
put 、resize 、get 、remove 、replace - 本篇不介绍树,会额外分析
HashMap 核心属性
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 | public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable { /** * table默认大小的阙值 */ static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16 /** * map容量的最大阙值 */ static final int MAXIMUM_CAPACITY = 1 << 30; /** * 默认的负载因子 */ static final float DEFAULT_LOAD_FACTOR = 0.75f; /** * 树化阙值 */ static final int TREEIFY_THRESHOLD = 8; /** * 树降级为链表的阙值 */ static final int UNTREEIFY_THRESHOLD = 6; /** * 哈希表的所有元素个数超过64,才允许树化 * 树化的标准条件:1. 链表长度>= 8 * 2. 哈希表元素个数超过64个才允许树化 */ static final int MIN_TREEIFY_CAPACITY = 64; /** * 哈希表 * 问题:什么时候初始化? */ transient Node<K,V>[] table; /** * 哈希表元素个数 */ transient int size; /** * 当前哈希表结构修改次数。(替换不会变) */ transient int modCount; /** * 扩容阙值,哈希表中的元素超过阙值,触发扩容 */ int threshold; /** * 负载因子,可设置 * threshold =capacity * loadFactor */ final float loadFactor; } |
HashMap 构造方法
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 | /** * 第一个参数:initialCapacity --> 使用者设置的哈希表的大小 * 第二个参数:loadFactor --> 设置的加载因子 */ 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; // 通过tableSizeFor计算出扩容阙值 this.threshold = tableSizeFor(initialCapacity); } public HashMap(int initialCapacity) { this(initialCapacity, DEFAULT_LOAD_FACTOR); } public HashMap() { this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted } public HashMap(Map<? extends K, ? extends V> m) { this.loadFactor = DEFAULT_LOAD_FACTOR; putMapEntries(m, false); } |
哈希表元素:Node
1 2 3 4 5 6 7 8 9 10 11 12 | static class Node<K,V> implements Map.Entry<K,V> { final int hash; // key经过扰动函数hash计算得出的hash值 final K key; // 要存储的key V value; // 要存储的value Node<K,V> next; Node(int hash, K key, V value, Node<K,V> next) { this.hash = hash; this.key = key; this.value = value; this.next = next; } |
如何计算扩容阙值: tableSizeFor 方法
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 | /** * Returns a power of two size for the given target capacity. * 返回当前>=cap的一个数字,并且是2的次方数 * * 假如输入cap的是27 * n = cap - 1 =26 * 26的二进制是 11010 => 即n的二进制是11010 * 运算规则: * n |= n >>> 1; 的意思是:n 按位"或运算" | n向右位移1位的结果,得出的结果,并同时赋值给n * 11010 |= 01101 => 11111 ( 01101 是n向右位移1位的结果,然后与n进行"或运算" 得出11111,并赋值给n。以下相同规则) * 11111 |= 00111 => 11111 * 11111 |= 00001 => 11111 * 11111 |= 00000 => 11111 * ..... 后面的位移都一样了 * * 最后n的二进制111111 * 二进制111111 = 31 * * 返回结果: * return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1; * 解释--> n=31 执行 (n >= MAXIMUM_CAPACITY) * (n=31)<MAXIMUM_CAPACITY 执行 n + 1 * 最后返回结果:32 */ 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; } |
如何增加hash的散列性:扰动函数hash()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | /** * 作用:让key的hash值高16位,参与路由运算。减少hash值的碰撞 * * 1. key=null 插入第一位 * 2. (h = key的hashCode) ^ (h右移16位) * 异或算法:相同返回0 不同返回1 * 例如: * h = 1001 0011 1011 1111 0000 0110 1000 0011 * h >>> 16 =>0000 0000 0000 0000 1001 0011 1011 1111 * * 1001 0011 1011 1111 0000 0110 1000 0011 * ^ * 0000 0000 0000 0000 1001 0011 1011 1111 * = * 1001 0011 1011 1111 1001 0101 0011 1100 * */ static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); } |
如何路由寻址(计算出桶位)
1 | 桶位 = (table.length-1)& hash |
HashMap核心方法 put 、 resize 、get 、remove 、replace
put -> putVal 源码
先看一下put数据时的大致流程图:
image.png
源码注释比较多,后面对putVal源码的执行顺序做了
先贴出源码流程图小结,方便对比源码查看流程
putVal源码流程图小结
对应源码中代码的执行顺序,可对比着源码查看:
image.png
putVal源码:
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 74 75 76 77 78 79 80 81 82 83 84 85 | /** * 问题: HashMap什么时候初始化哈希表的? * 答:第一次调用put时初始化。 * 问题:为什么第一次put才去初始化哈希表? * 答:为什么第一次put才会初始化因为哈希表初始化会占用很大内存,用户可能只是new HashMap而没有使用 * * 路由寻址作用:通过路由寻址确定要插入的元素,在table中应该插入的桶位 * 路由寻址公式 : index下标 = (table-1) & hash */ final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node<K, V>[] tab; // 临时table Node<K, V> p; // 临时存储的元素对象(解释:通过路由寻址,找到的元素) int n, i; // n:是临时的table长度 i:通过路由寻址赋值的table的桶位(即:table表中的下标) // 分析1 --> table不存在初始化哈希表 // 解释 --> table==null 或 table长度为0。哈希表不存在,触发扩容机制。 同时完成tab、n 的赋值 if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; /** * put数据时的2种情况: * 路由寻址算法:桶位 = (table-1) & hash * 1.该桶位不存在数据 if * 2.该桶位已经存在数据 else * */ // 分析2 --> 这个桶位不存在元素直接存入该桶位 // 解释 --> 通过路由寻址 确定index, 检测index在table中元素是否存在。不存在直接存入该桶位。同时完成 p 的赋值 if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); else { // 分析3 --> p 元素存在值,则属于 链表、红黑树 情况 // 解释 --> p 在上面if判断中已经赋值。(即:当前该桶位已经存在的元素) Node<K, V> e; // 临时的元素对象 K k; // 临时key // 分析3.1 --> table 中已经存在元素和要存储的元素重复 /** * p.hash:当前元素的key的hash值 * 解释 --> 条件1: p.hash == hash : 当前table[index]元素对象的key的hash值和要存储元素的key的hash值相同 * 解释 --> 条件2: ((k = p.key) == key || (key != null && key.equals(k)))): 当前元素的key和要存入元素的key相等 或 (要存入元素的key!=null 且 要存入元素的key和当前桶位的元素的key相同) * 满足2个条件,说明要存入的元素和当前桶位的元素相同 * 对临时元素对象e赋值 * 后面对e进行判断,e!=null 的情况,会进行覆盖 * */ if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p; //分析3.2 --> 检测p元素是否在树中 else if (p instanceof TreeNode) e = ((TreeNode<K, V>) p).putTreeVal(this, tab, hash, key, value); else { // 分析 3.3 --> 链表。 说明链表头元素和和当前要存入的元素不同 // 解释 --> 遍历链表 for (int binCount = 0; ; ++binCount) { // 分析 3.3.1 --> 遍历整个链表 p.next==null 没找到与当前要插入的的key的node // 解释 --> 形成链表的关键,就是靠Node元素内的next来进行指向下一个元素的 if ((e = p.next) == null) { p.next = newNode(hash, key, value, null); //分析 3.3.2 --> 判断插入当前元素后,检查是否达到树化标准:1.链表>8 2.table 元素个数超过64 if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st treeifyBin(tab, hash);// 树化 break; } // 3.3.3 说明在链表中找到了,与当前要插入的key相同的node元素。break结束循环,后续要替换操作 if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; } } // 3.4 e != null 新值替换旧值 并返回老值 if (e != null) { // existing mapping for key V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); return oldValue; } } // 4. 记录对table操作的次数 ++modCount; // 5. 再次检测是否需要扩容 if (++size > threshold) resize(); afterNodeInsertion(evict); return null; }w |
putVal源码流程文字小结
-
map第一次put时,哈希表不存在元素,会触发扩容机制
-
通过路由寻址确定要插入元素的桶位,当前桶位无元素,则直接插入该桶位
-
前2个条件不符合,说明该桶位已经存在元素
3.1 通过比对要插入的Node和当前桶位Node的key的hash值 && key是否相同,相同则对临时Node赋值,后续会进行新值替换
3.2 已经树化
3.3 遍历链表
1
2
3
4
53.3.1 遍历查找与当前要插入的Node是否有相同,(e=p.next)=null 说明没有存在的,插入链表
3.3.2 插入后,检测是否满足树化标准条件,满足的话进行树化。
3.3.3 同3.1的比对逻辑一样。 是相同元素则break 结束循环3.4 e != null 新值替换旧值 并返回老值
-
记录对table操作的次数
-
再次检测是否需要扩容:符合条件进行扩容
resize 扩容机制
扩容核心做了2件事情:
image.png
resize源码:
源码中注释已经写很详细。需要看官,脑海中有个结构。这段源码后面会放出美团技术团队博文中的一张图:
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 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 | /** * 问题:为什么需要扩容? * 答:为了解决hash冲突导致的链化影响查询效率的问题,扩容会缓解该问题 */ final Node<K, V>[] resize() { Node<K, V>[] oldTab = table; // 临时变量:扩容前table int oldCap = (oldTab == null) ? 0 : oldTab.length; // 临时 扩容前table长度 int oldThr = threshold; // 临时变量:触发扩容前阙值 int newCap, newThr = 0; //newCap = 临时变量:扩容后table长度 newThr = 临时变量:扩容之后,下次触发扩容阙值 后面newThr会赋值给threshold /* ----------------- 计算 newCap newThr -----------------*/ // 分析 1 --> 计算新的newCap值(临时新table长度) 、newThr值(临时新扩容阙值) // 分析 1.1 --> 哈希表已经初始化过 if (oldCap > 0) { // 分析 1.1.1 --> 哈希表长度大于定义最大范围(这种情况一般不会发生) // 解释 --> 向左位移1位即:扩大一倍。所以 1 << 30是一个很大的数字 if (oldCap >= MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return oldTab; } // 分析 1.1.2 --> 向左位移1位(扩大一倍) // 解释 --> 满足2个条件:1.当前哈希表的大小 < 定义的最大值 2. 当前哈希表的大小 > 哈希表的最小初始值 else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) newThr = oldThr << 1; // double threshold 位移后的扩容阙值,赋值给newThr } // 分析 1.2 --> 通过HashMap其他三个构造方法初始化. //解释 --> 只有new HashMap() 没有对threshold进行初始化 else if (oldThr > 0) // initial capacity was placed in threshold newCap = oldThr; else { // 分析 1.3 --> // 即:oldCap=0 , 当前table=null // 解释 --> 说明当前是通过new HashMap() 初始化的。 // newCap : table // newThr 扩容阙值: 默认大小 * 负载因子 newCap = DEFAULT_INITIAL_CAPACITY; newThr = (int) (DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); } // 分析 --> 1.4 对上述计算newThr进行检测。如果条件成立 需要重新计算出newThr if (newThr == 0) { float ft = (float) newCap * loadFactor; newThr = (newCap < MAXIMUM_CAPACITY && ft < (float) MAXIMUM_CAPACITY ? (int) ft : Integer.MAX_VALUE); } threshold = newThr; /*----------------- 扩容开始 -----------------*/ //分析 2--> 真正开始扩容 @SuppressWarnings({"rawtypes", "unchecked"}) Node<K, V>[] newTab = (Node<K, V>[]) new Node[newCap]; table = newTab;//扩容之后的哈希表 赋值给 table if (oldTab != null) {// 确定原来的哈希表不为null才进行扩容 for (int j = 0; j < oldCap; ++j) { Node<K, V> e; if ((e = oldTab[j]) != null) { // 当前桶位存在元素 oldTab[j] = null; // 把当前桶位置为null,方便回收。上面if判断已经赋值给e //2.1. e.next == null 说明当前桶位只有一个数据(不是链表、树) // 把当前元素,通过路由寻址找到在新哈希表中的桶位,直接存入该桶位 if (e.next == null) newTab[e.hash & (newCap - 1)] = e; //2.2. 该节点,已经树化 else if (e instanceof TreeNode) ((TreeNode<K, V>) e).split(this, newTab, j, oldCap); //3.3. 链表 else { // preserve order //底位链表:存放在扩容之后的数组的下标的位置,与当前数组下标的位置一致(例如:table 16扩容到32 ,假如15下标位置的数据,扩容后数据还是在15这个位置) Node<K, V> loHead = null, loTail = null; //高位链表:存放在扩容之后的数组的下标的位置 --> 当前数组下标位置+扩容之前数组的长度 Node<K, V> hiHead = null, hiTail = null; Node<K, V> next; do { next = e.next; //对链表进行拆分,分别存入低位链表、高位链表 /** * 例: * e.hash存在2种情况: 高位要么是1要么是0 * 1. 1 1111 * 2. 0 1111 * 假设oldCap=16 =>二进制: 1 0000 * * &运算: * 1 1111 * & 1 0000 * = 1 0000 =>16 说明高位是1 存入高链 * * 0 1111 * & 1 0000 * = 0 0000 =>结果为0 说明高位是0 存入低链 * */ 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; // 目的是把链断开 存入低链时,next还指向一个高位链的数据,所以需要断开 newTab[j] = loHead; } if (hiTail != null) { hiTail.next = null;// 目的是把链断开 newTab[j + oldCap] = hiHead; } } } } } return newTab; } |
美团博文中的图:
image.png
get -> getNode
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 | final Node<K, V> getNode(int hash, Object key) { Node<K, V>[] tab; // 临时table Node<K, V> first, e; // first -> 头元素 int n; // table长度 K k; /** * 两个条件: * 1. 哈希表不能为null * 2. 路由寻址,桶位不能为null * */ if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) { /** * 第一种情况: * 检查当前桶位元素 * 1. first.hash == hash => 当前桶位的Node的key的hash值和要取的key的hash值相同 * 2. 当前桶位的Node的key和要取的key相同 * 满足2条件则返回当前桶位的Node * 进入此判断: * 说明当前桶位只有一个元素 */ if (first.hash == hash && // always check first node ((k = first.key) == key || (key != null && key.equals(k)))) return first; /** * 第二种情况: * 当前桶位的Node的next不为null,则当前桶位是 链表 或 树 * */ if ((e = first.next) != null) { // 树查找 if (first instanceof TreeNode) return ((TreeNode<K, V>) first).getTreeNode(hash, key); // 链表查找 do { /** * 1. first.hash == hash => 当前桶位的Node的key的hash值和要取的key的hash值相同 * 2. 当前桶位的Node的key和要取的key相同 * */ if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) return e; } while ((e = e.next) != null); } } return null; } |
remove -> removeNode
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 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 | 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; // p -> 根据寻址结果得到的桶为元素Note // n -> 哈希表长度 // index -> 寻址结果 /*---------分析 1 --> 查找要删除的Node ---------*/ /** * 两个条件: * 1. 哈希表不能为null * 2. 路由寻址,桶位不能为null * */ if ((tab = table) != null && (n = tab.length) > 0 && (p = tab[index = (n - 1) & hash]) != null) { Node<K, V> node = null, e; // //e --> 当前桶位元素的下一个元素 K k; V v; /** * 分析1.1 --> 同getNode的比对逻辑是一样的 * 赋值给临时node * */ if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) node = p; // /** * 分析1.2 --> * 当前桶位的Node的next不为null,则当前桶位是 链表 或 树 * */ 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)))) { // 链表中到元素 break结束循环 node = e; break; } p = e; } while ((e = e.next) != null); } } /*---------分析 2 --> 删除找到的Node ---------*/ /** * Node不为null,说明找到了要删除的Node * */ // Node不为null 比对要删除的value是否相等 if (node != null && (!matchValue || (v = node.value) == value || (value != null && value.equals(v)))) { // 分析 2.1 --> 树删除 if (node instanceof TreeNode) ((TreeNode<K, V>) node).removeTreeNode(this, tab, movable); //分析 2.2 --> 把node的下一个元素,指向当前桶位 // 解释 --> 只有一种情况,就是分析1.1 -->这种情况,当前的桶位元素即为要删除的情况 else if (node == p) // tab[index] = node.next; //分析 2.3 --> (链表中间元素删除)将当前桶位要删除元素的上一个元素next指向node的下一个元素 /** * 解释-->:1,2,3三个元素 * 关系: 1.next指向2 * 2.next指向3 * => 2是中间元素 * 删除:2是要删除的元素: * => 把1的next指向2的next(2的next是3) 即: 1.next = 3 * 这样就完成链表中间元素的删除 * * */ else p.next = node.next; // 同步哈希表的操作次数 ++modCount; // 同步哈希表的长度 --size; afterNodeRemoval(node); return node; } } return null; } |
replace
getNode获取元素
1 2 3 4 5 6 7 8 9 10 11 12 | @Override public boolean replace(K key, V oldValue, V newValue) { Node<K,V> e; V v; if ((e = getNode(hash(key), key)) != null && ((v = e.value) == oldValue || (v != null && v.equals(oldValue)))) { // 替换 e.value = newValue; afterNodeAccess(e); return true; } return false; } |