文章目录
结构
hashMap 空间使用程度相关参数含义
size | threshold | table.length |
---|
size:代表已包含的成员数量
threshold:代表着需要触发扩容的成员数量
table.length:代表整个容器的总数量
一个容器,固有属性必须要有总容量和已使用容量,threshold并非必须。
槽位概念
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
---|
hashMap底层存储成员使用的是数组,而槽位就代表着成员所对应的数组索引
确定成员槽位的方法:hash()&table.length(效果等价Hash()%table.length,使用&是因为计算机能直接识别&操作,%操作需要转换),通过hash()获取到int数值然后对容器容量进行与来定位到槽位(进行与运算结果必定在容器容量内)
Hash()方法
static final int hash(Object key) {
int h;
//对key进行hash,然后将hash后的高16位进行异或操作
/**
* 目的:因为大部分的hash表的长度是低于16位的,如果直接用hash结果,那
* 么就意味着,只有低16参与了离散,离散效果就大大降低,针对于这种情况,
* 将高16也参与运算,就能一定程度的避免上面的问题。
**/
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
putVal() 方法
/**
* Implements Map.put and related methods
*
* @param hash hash for key
* @param key the key
* @param value the value to put
* @param onlyIfAbsent if true, don't change existing value
* @param evict if false, the table is in creation mode.
* @return previous value, or null if none
*/
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
//判断hash表是否为空,如果为空进行resize操作
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//hash值与容器容量取余得到槽位,并且该槽是空,则直接新增
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
//如果不是空的,那么就要考虑链式or红黑树两种情况了
else {
Node<K,V> e; K k;
//p为该槽位的第一个节点
//如果第一个成员与放入的成员是一样的
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)
//红黑树的put方案
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
//单向链表的put方案,比较简单,就是加入链尾部和判断是否需要转换为红黑树
for (int binCount = 0; ; ++binCount) {
//如果不存在相同的成员,则在末尾新增该节点
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
//新增之后发现,整个链的长度大于等于8,槽位大于64则变更为红黑树,小于则扩大槽位
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
//如果成员已经存在则跳出,交由后续,统一处理存在成员的情况
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
//向下一个链表成员前进
p = e;
}
}
//如果该成员已经存在map中
if (e != null) { // existing mapping for key
//获取以前的值
V oldValue = e.value;
//如果允许覆盖,或则oldValue其实是空值
if (!onlyIfAbsent || oldValue == null)
//将值覆盖以前的老值
e.value = value;
//HashMap没有使用到,LinkHashMap在使用,这里不做分析
afterNodeAccess(e);
return oldValue;
}
}
//操作数加一,在遍历操作时依靠该参数判断是否有其他线程在操作容器,快速报错
++modCount;
//如果新增成员之后,数量超过了阈值则进行扩容
if (++size > threshold)
resize();
//HashMap没有使用到,LinkHashMap在使用,这里不做分析
afterNodeInsertion(evict);
return null;
}
总结
- 容器是否初始化,没有初始化就进行初始化 【初始化容器】
- 放入的槽位是否为空? 【区分新增,修改】
- 为空直接放入并结束 【处理新增情况】
- 不为空,是红黑树还是链表 【处理修改情况,获取修改元素】
3.1 是红黑树则以红黑树的方式放入 【处理修改情况,获取修改元素】
3.2 是链表则往链表末尾添加,添加完如果整个链长于8则转换为红黑树 【处理修改情况,获取修改元素】 - 获取旧值,替换新值 【根据条件决定元素是否放入】
resize() 方法
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
//获取以前的hash表容量
int oldCap = (oldTab == null) ? 0 : oldTab.length;
//设置上次记录的进行扩容的阈值
int oldThr = threshold;
int newCap, newThr = 0;
//如果以前存在容量
if (oldCap > 0) {
//并且容量等于或超出的最大容量值的一半,
//2*MAXIMUM_CAPACITY=Integer.MAX_VALUE
if (oldCap >= MAXIMUM_CAPACITY) {
//下次进行扩展的阈值设置为最大值
threshold = Integer.MAX_VALUE;
//同时返回老的hash表,意味着不在进行扩容,超出直接报错
return oldTab;
}
//扩展容量为当前两倍,如果扩展之后容量小于最大值一半,并且当前容
//量大于了初始容量(意味着进行了一次扩容)
//则将阈值也扩大一倍
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
//如果oldCap < DEFAULT_INITIAL_CAPACITY (意味没有进行过一次扩容)
//则newThr=0,新的阈值为0
//如果newCap超过了MAXIMUM_CAPACITY(容器最大容量的一半)
//则newThr=0,新的阈值为0
}
//如果以前没有容量,但是设置了阈值(这种场景很奇怪),则将容量与阈值对齐
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
//如果容量和阈值都无,着设置默认值(这种场景一样奇怪)
else { // zero initial threshold signifies using defaults
//默认容量16
newCap = DEFAULT_INITIAL_CAPACITY;
//默认阈值 容量*过载因子(0.75*16)
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
/**
* 目的:用于处理三种情况
* 1.没有进行过一次扩容
* 2.容量扩展后已经超过最大值得一半
* 3.当前没有容量,但存在阈值
**/
if (newThr == 0) {
//扩展阈值为,0.75*最新的容器总量
float ft = (float)newCap * loadFactor;
//如果最新的容器总量小于最大容量的一半并且阈值低于容器总量的一半
//则扩展阈值=0.75*最新容器总量,否则为最大值(意味则直到报错也不在进行扩展)
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;
//如果该hash槽里面只有一个对象
if (e.next == null)
//直接对其进行重新hash
//1.如果扩展的高位为0,则其实还是在原来的槽中
//2.如果扩展的高位为1,则需要放入新的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<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
//遍历整个链
do {
next = e.next;
//如果成员高位为0(hash后还在原来的槽中),则放到lo中
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
} //如果成员高位为1(hash后还在原来的槽中),则放到hi中
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;
}
总结
- table.length(容器容量):要么默认16,要么以当前的容器*2,直到第一次超过Integer最大值的一半
- threshold(扩展阈值):容器容量*0.75,直到容器容量超过Integer最大值的一半,就设置为Integer.MAX_VALUE
- size(使用量):每新增一个成员就加一,达到threshold就触发扩容
逻辑步骤
- 根据当前容器属性,获取到容器容量,扩展阈值。
- 容器扩展之后,对容器中的成员重新计算hash分配槽位
2.1 槽中只有单个成员(未形成链)
2.2 槽中成员已形成红黑树
2.3 槽中成员形成链,重新计算每一个链上成员分配到新槽位中重新形成链
removeNode()方法
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;
//p为槽位头部成员
//如果头部成员就是要移除的成员,则获取到成员(离散程度好,大概率就是这个)
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
node = p;
//在后续成员中查找要移除的成员
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;
} while ((e = e.next) != null);
}
}
//如果找到成员,并且不需要匹配value是否一致
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)
//指向链表的第二个成员
tab[index] = node.next;
else
//在链上进行拼接
p.next = node.next;
//操作数加一
++modCount;
//使用量减一
--size;
//HashMap未使用,LinkHashMap在使用
afterNodeRemoval(node);
return node;
}
}
return null;
}
总结
- 是否头部成员就是要移除的成员,获取成员
- 非头部成员,那么结构是链表还是红黑树
2.1 以红黑树方式查询成员
2.2 以链表方式查询成员 - 如果找到成员,匹不匹配value值
3.1 条件都满足,如果是红黑树,以红黑树的方式移除
3.2 条件都满足,如果是链表,以链表的方式移除
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) {
//hash离散程度高,那么就大概率是第一个
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;
}
KeySet 和 Values
final class KeySet extends AbstractSet<K> {
public final int size() { return size; }
public final void clear() { HashMap.this.clear(); }
public final Iterator<K> iterator() { return new KeyIterator(); }
public final boolean contains(Object o) { return containsKey(o); }
public final boolean remove(Object key) {
return removeNode(hash(key), key, null, false, true) != null;
}
public final Spliterator<K> spliterator() {
return new KeySpliterator<>(HashMap.this, 0, -1, 0, 0);
}
public final void forEach(Consumer<? super K> action) {
Node<K,V>[] tab;
if (action == null)
throw new NullPointerException();
if (size > 0 && (tab = table) != null) {
//获取循环前的操作证值
int mc = modCount;
for (int i = 0; i < tab.length; ++i) {
for (Node<K,V> e = tab[i]; e != null; e = e.next)
action.accept(e.key);
}
//遍历完成后抛出异常,意味着accept里面的执行结果还是执行完了
if (modCount != mc)
throw new ConcurrentModificationException();
}
}
}
参考KeySet
final class Values extends AbstractCollection<V> {
public final int size() { return size; }
public final void clear() { HashMap.this.clear(); }
public final Iterator<V> iterator() { return new ValueIterator(); }
public final boolean contains(Object o) { return containsValue(o); }
public final Spliterator<V> spliterator() {
return new ValueSpliterator<>(HashMap.this, 0, -1, 0, 0);
}
public final void forEach(Consumer<? super V> action) {
Node<K,V>[] tab;
if (action == null)
throw new NullPointerException();
if (size > 0 && (tab = table) != null) {
int mc = modCount;
for (int i = 0; i < tab.length; ++i) {
for (Node<K,V> e = tab[i]; e != null; e = e.next)
action.accept(e.value);
}
if (modCount != mc)
throw new ConcurrentModificationException();
}
}
}
整体总结
1.槽位筛选,进行hash后在大多数场景容器长度总是在低16位,将高16进行异或(手段)使高16位的影响能扩散到低16位
2.对于hash冲突的成员,hashMap采用的是链表,如果hash设置不合理,会将hashMap退化为数组(所有put成员的hash一样),查询性能从O(1)退化位O(N),基于此将链表长度高于8位的重新组装成红黑树,查询性能就可以只退化到O(logN)。
3.modCount属性用于判断在遍历过程中是否有其他线程同步操作容器,并且抛出异常的时机是在循环执行完毕之后,不是实时抛出,意味着循环里面的方法还是执行完了 (理解与实现存在偏差)
遗留问题
hashMap本身不是线程安全的,意味着正常使用情况下,同一时刻只有一个线程进行使用,为何需要一个threshold来帮助扩容?如果当容器填满后进行扩容不可行吗?
按照目前的理解,提前扩容能避免错误使用多线操作容器下,不抛出越界错误。
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/121859.html