前一篇博文 JDK容器学习之HashMap (一) : 底层存储结构分析 分析了HashMap的底层存储数据结构java
经过
put(k,v)
方法的分析,说明了为何Map底层用数组进行存储,为何Node
内部有一个next
节点,这篇则将集中在读写方法的具体实现上node
本片博文将关注的重点:数组
value
的实现逻辑table
数组如何自动扩容
get(key)
做为map最经常使用的方法之一,根据key获取映射表中的value,一般时间复杂度为o(1)
数据结构
在分析以前,有必要再把HashMap
的数据结构捞出来看一下ide
根据上面的结构,若是让咱们本身来实现这个功能,对应的逻辑应该以下:学习
table
数组中的位置next
节点的key,直到找到为止jdk实现以下测试
public V get(Object key) { Node<K,V> e; return (e = getNode(hash(key), key)) == null ? null : e.value; } final Node<K,V> getNode(int hash, Object key) { Node<K,V>[] tab; Node<K,V> first, e; int n; K k; // 判断条件 if 内部逻辑以下 // table 数组已经初始化(即非null,长度大于0) // 数组中根据key查到的Node对象非空 if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) { if (first.hash == hash && // always check first node ((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; }
上面的逻辑算是比较清晰,再简单的划一下重点this
hash(key) & (table.length - 1)
node.hash == hash(key) && (node.key == key || (key!=null && key.equals(node.key))
and == or equals
上面的逻辑中,当出现hash碰撞时,会判断数组中的Node
对象是否为 TreeNode
,若是是则调用 TreeNode.getTreeNode(hash,key)
方法.net
那么这个TreeNode有什么特殊的地方呢?code
TreeNode
分析
TreeNode
依然是HashMap
的内部类, 不一样于Node的是,它继承自LinkedHashMap.Entry
,相比较与Node
对象而言,多了两个属性before, after
TreeNode对象中,包含的数据以下(将父类中的字段都集中在下面了)
// Node 中定义的属性 final int hash; final K key; V value; Node<K,V> next; // --------------- // LinkedHashMap.Entry 中的属性 Entry<K,V> before, after; // --------------- // TreeNode 中定义的属性 TreeNode<K,V> parent; // red-black tree links TreeNode<K,V> left; TreeNode<K,V> right; TreeNode<K,V> prev; // needed to unlink next upon deletion boolean red;
方法比较多,实现也很多,可是看看方法名以及注释,很容易猜到这是个什么东西了
红黑树
具体方法实现身略(对红黑树实现有兴趣的,就能够到这里来膜拜教科书的实现方式)
普通的Node就是一个单向链表,所以HashMap的结构就是上面哪一种
TreeNode是一颗红黑树的结构,因此对上面的图走一下简单的改造,将单向链表改为红黑树便可
博文 JDK容器学习之HashMap (一) : 底层存储结构分析 对于添加kv对的逻辑进行了说明,所以这里将主要集中在数组的扩容上
扩容的条件: 默认扩容加载因子为(0.75),临界点在当HashMap中元素的数量等于table数组长度*加载因子,长度扩为原来的2倍
数组扩容方法, 实现比较复杂,先撸一把代码,并加上必要注释
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 } else if (oldThr > 0) { // initial capacity was placed in threshold 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) { // 若Node节点没有出现hash碰撞,则直接塞入新的数组 newTab[e.hash & (newCap - 1)] = e; } else if (e instanceof TreeNode) { // 对于出现hash碰撞,且红黑树结构时,须要从新分配 ((TreeNode<K,V>)e).split(this, newTab, j, oldCap); } else { // preserve order 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) { // 新的位置相比原来的新增了 oldCap 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; }
上面的逻辑主要划分为两块
说明
这个扩容的逻辑仍是比较有意思的,最后面给一个测试case,来看一下扩容先后的数据位置
删除的逻辑和上面的大体相似,显示肯定节点,而后从整个数据结构中移除引用
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; // 删除的前置条件: // 1. 数组已经初始化 // 2. key对应的Node节点存在 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节点即为目标 node = p; } else if ((e = p.next) != null) { // hash碰撞,目标可能在链表or红黑树中 // 便利链表or红黑树,肯定目标 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; } while ((e = e.next) != null); } } if (node != null && (!matchValue || (v = node.value) == value || (value != null && value.equals(v)))) { // 找到目标节点,直接从数组or红黑树or链表中移除 // 不改变Node节点的内容 if (node instanceof TreeNode) ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable); else if (node == p) tab[index] = node.next; else p.next = node.next; ++modCount; --size; afterNodeRemoval(node); return node; } } return null; }
上面的几个经常使用方法的逻辑大体相同,核心都是在如何找到目标Node节点,其中比较有意思的一点是数组的扩容,旧元素的迁移逻辑,下面写个测试demo来演示一下
首先定义一个Deom对象,覆盖hashCode
方法,确保第一次从新分配数组时,正好须要迁移
public static class Demo { public int num; public Demo(int num) { this.num = num; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Demo demo = (Demo) o; return num == demo.num; } @Override public int hashCode() { return num % 3 + 16; } } @Test public void testMapResize() { Map<Demo, Integer> map = new HashMap<>(); for(int i = 1; i < 12; i++) { map.put(new Demo(i), i); } // 下面这一行执行,并不会触发resize方法 map.put(new Demo(12), 12); // 执行下面这一行,会触发HashMap的resize方法 // 由于 hashCode值 & 16 == 1,因此新的位置会是原来的位置+16 map.put(new Demo(13), 13); }
实际演示示意图
hash & (len - 1) === hash % len
key1 == key2 or key1.quals(key2)
loadFactor
; loadFactory
通常为 0.75(hash % 原长度 == 0)
(hash % 原长度 == 1)
相关博文
扫一扫二维码,关注 小灰灰blog