HashMap 那点事
HashMap
一、默认参数
// 默认初始容量16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
// 最大容量,容量必须是 2 的倍数,且小于最大容量。要是大于则取最大容量。
static final int MAXIMUM_CAPACITY = 1 << 30;
// 默认负载因子的值是 0.75,当负载因子是这个数的时候 hash 分布的更加均匀,泊松分布
static final float DEFAULT_LOAD_FACTOR = 0.75f;
// 当大于此值的时候链表转化为红黑树
static final int TREEIFY_THRESHOLD = 8;
// 当小于此值的时候红黑树转化为链表
static final int UNTREEIFY_THRESHOLD = 6;
// 控制着转化红黑树的条件,只有节点总数大于此值的时候,并且满足单个链表长度大于8才会转化红黑树
static final int MIN_TREEIFY_CAPACITY = 64;
二、初始化那点事
HashMap的初始化种类还是挺全的,有参的,无参的,半参的都有。如果使用无参的构造函数的话,存储数据的 table 不会被初始化,只有等到真正使用到的时候才会去初始化。
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() {
// 这里只会将负载因子赋值,并没有去 tableSizeFor
this.loadFactor = DEFAULT_LOAD_FACTOR;
}
tableSizeFor
这个方法是将当前值 -1 后,从最左边开始不为零的位数开始向右填充 1,最后再 + 1 完成收尾工作。这番操作下来将会得到大于 cap 的最大的 2 幂次方(7 -> 8,8 -> 8,10 -> 16)。
// 以 10 为栗子来说的话 cap = 0000 0000 0000 1010
static final int tableSizeFor(int cap) {
int n = cap - 1; // 0000 0000 0000 1001 9
n |= n >>> 1; // 0000 0000 0000 1101 13
n |= n >>> 2; // 0000 0000 0000 1111 15
n |= n >>> 4; // 15
n |= n >>> 8; // 15
n |= n >>> 16; // 15
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1; // 16
}
三、增(put是个什么玩法)
会根据增加一个值的流程来逐步看方法
1、put
当调用 put 的方法的时候,会先 hash 得到要存放的位置,然后再进行逐步的操作
// 会将旧值返回出来,如果没有旧值则返回 null
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
2、hash 怎么跟 hashCode 还不一样?
将 key 的 hashCode 与其高 16 位的值做 ^ 操作,得到的值为真正的 hash 值,让高 16 位参与 hash 运算会减小 hash 冲突的概率。
/**
* 1001 ^ 0010 -> 1011
* 9 ^ 2 -> 11
**/
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
3、putVal 好长啊,还要被 put 调用
这个方法可就有点意思了,包含的知识点也比较多,首先从他的方法注解看起(翻译至方法上),至于步骤什么的都放在了方法上的注释上,这里就先总结一下 HashMap 的 put :
- 先做判空处理,如果为空就先扩容
- 根据 hash 找 table 的位置,如果找到的位置上是 nulll 的话就直接赋值
- 找到的位置有数据了,就循环遍历那个 链表/红黑树
- 如果没找到相同的 hash 与 key,则将最后一个节点的 next 指向新建的 Node,并且判断是否是大于转化成树的值,如果大于则开始使用 treeifyBin 方法尝试将链表转化为树结构,如果大小小于最小转化树结构的阈值,则进行一次扩容,而不是树结构的转化。
- 如果找到了的话直接结束循环
- 判断是找到相同值结束还是未找到值结束,如果是找到值结束则判断是否可以覆盖旧值,可以则覆盖掉,并且 return 旧值,不可以的话则直接 return 旧值
- 对 modCount +1,并且判断是否要扩容,需要则扩容
- 返回 null
- 流程图点击一下
/**
* Implements Map.put and related methods.
*
* @param hash key经过 hash 方法后的值
* @param key key值
* @param value 将要被 put 的value值
* @param onlyIfAbsent 如果为真,则对现有的值不进行改变
* @param evict 如果为false,则表处于创建模式。这里在 hashmap 中并未使用,在linked 才有用
* @return previous value, or null if none
*/
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
// 保存数据的 Node 数组
Node<K,V>[] tab;
Node<K,V> p;
// 用来保存 table 的长度
int n, i;
// 疯狂的赋值加判断,如果 table 为空则先扩容
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// n 是 2 的幂次方,所以 (n - 1) & hash 相当于 hash % n
// 如果这里取得值是null的话,说明此空间还没有被占领,可以直接创建一个 Node 并且赋值给当前位置
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
// 取到的内容不为空的情况,e属于中间值
Node<K,V> e;
K k;
// 如果 hash 相同,并且 key 也相同的话,就直接将值覆盖掉
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
// 判断是否是红黑树的节点,如果是的话走红黑树节点的逻辑(暂时一放)
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
// 当判定为是链表结构的时候,找链表的最后一个值
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
// 如果链表长度大于等于 8 的话,就开始尝试转化链表为红黑树
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;
}
}
// 结束查找与操作,当查到 hash 与 key.hashCode() 相同的对象的时候执行以下操作
if (e != null) { // existing mapping for key
V oldValue = e.value;
// 判断是否可以覆盖,可以覆盖的话就直接去覆盖值
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
// 用以判断是否 fast-fail 的标志位 +1
++modCount;
// 如果增加新值后的大小大于 容量*负载因子 的话就进行扩容
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
4、resize 扩容怎么这么麻烦 ?
很巧妙的点是在对旧值迁移到新集合中 ,如果要迁移数据的 hash 与原先集合的 最高位
最高位
做&
运算,如果为 0 则表示不需要迁移,为 1 的话也只是将位置直接计算出来现在的位置 + 旧集合的大小
,不用再执行 hash 操作。
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
// 这里都是按照 length 来进行操作的
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
if (oldCap > 0) {
// 最大也不能超过 Integer.MAX_VALUE
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
// 新值等于旧值 << 1,并且与 1<<30 做对比,还要判断是否大于 16(初始大小是 16)
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
// 可以的话扩容至旧值的 2 倍
newThr = oldThr << 1;
}
else if (oldThr > 0)
// initial capacity was placed in threshold
// 如果旧的阈值有的话,就直接赋值给要扩展的新的值
newCap = oldThr;
else {
// zero initial threshold signifies using defaults
// 如果都没有的话就都取默认的值
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;
// 如果旧的数据集不为 null 的话就开始数据的转移
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)
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;
// 这是一个很巧妙的点,将 e 的hash值与旧值做 & 操作,因为是 2倍扩容,所以如果是0的话就不需要移动table上的位置
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;
newTab[j] = loHead;
}
// 新的位置是 旧值 + 旧集合的大小
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
5、将链表尝试转化为红黑树的黑势力 treeifyBin
为啥叫做尝试性转换为
树结构
呢,因为有MIN_TREEIFY_CAPACITY
给限制住了,如果小于次参数也只能乖乖地扩容去了
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)
resize();
else if ((e = tab[index = (n - 1) & hash]) != null) {
TreeNode<K,V> hd = null, tl = null;
do {
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);
}
}
至此添加的逻辑也就这样了,但是又有很大的坑没有被填补,红黑树是怎么进行添加操作的呢?等以后再来填吧。
6、欣赏一下 Node 的容貌
Node 里面保存了 hash ,key 与 value ,其中 hash 是 hashCode 的高位与低位算出来的,赋值后即变为不可更改的状态,与 key 一样。
这里的 value 与 next 不为 final 是因为还需要覆盖与地址的重新指向。
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V 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;
}
public final K getKey() { return key; }
public final V getValue() { return value; }
public final String toString() { return key + "=" + value; }
public final int hashCode() {
return Objects.hashCode(key) ^ Objects.hashCode(value);
}
public final V setValue(V newValue) {
...
}
public final boolean equals(Object o) {
...
}
}
7、其他 put 的风采
对于 put 整个集合的话,都会用到以下的这个方法
final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
int s = m.size();
if (s > 0) {
if (table == null) { // pre-size
float ft = ((float)s / loadFactor) + 1.0F;
int t = ((ft < (float)MAXIMUM_CAPACITY) ?
(int)ft : MAXIMUM_CAPACITY);
if (t > threshold)
// 扩容至 t 的容量
threshold = tableSizeFor(t);
}
else if (s > threshold)
resize();
for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
K key = e.getKey();
V value = e.getValue();
// 遍历写入,这里不会再扩容,因为已经提前扩容过了
putVal(hash(key), key, value, false, evict);
}
}
}
四、删(remove 是怎么用的?)
从 remove 方法入手,看 remove 要执行哪些操作,调用哪些方法
1、remove 顶层方法
public V remove(Object key) {
Node<K,V> e;
// 还是先计算 hash 值,并且不会去判断 value 的值是否与被删除的值相同
return (e = removeNode(hash(key), key, null, false, true)) == null ?
null : e.value;
}
2、removeNode
这个方法相比于 putVal 方法可以说还算简单一些了,也就是先找到要移除的 Node 的位置,然后去移除掉,分三种情况移除而已
- 如果是树结构则直接走树的移除方法
- 如果是链表结构,但是是 Tab 结构上的第一个节点,则直接指向第一个节点的 next 节点
- 如果是链表上的节点,则执行链表移除节点的方式。
只要没有真正移除数据,就不会去修改 modCount 的结构,也不会引起 fast-fail 的结果。
/**
* Implements Map.remove and related methods.
*
* @param hash hash for key
* @param key the key
* @param value the value to match if matchValue, else ignored
* @param matchValue 如果为 True 的话就会匹配 value的值,只有相同才会去删除
* @param movable if false do not move other nodes while removing
* @return the node, or null if none
*/
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;
// 找符合条件的(hash & key)的 Node
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);
}
}
if (node != null && (!matchValue || (v = node.value) == value ||
(value != null && value.equals(v)))) {
if (node instanceof TreeNode)
// tree 的删除逻辑
((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
else if (node == p)
// 如果是 tab 位的第一个 Node 则直接指向下一个Node(可能为 null)
tab[index] = node.next;
else
// 链表方式移除数据
p.next = node.next;
++modCount;
--size;
afterNodeRemoval(node);
return node;
}
}
return null;
}
五、改(replace)
相比于增加的花里胡哨的操作,修改的操作确实是简单了很多,找到要修改的元素,如果找不到就返回 F,找到了之后将 Node 的 Value 替换成传入的 V 即可,如果调用带有旧值的方法的话,会有一个比较旧值的操作,相同才会去替换。
@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;
}
@Override
public V replace(K key, V value) {
Node<K,V> e;
if ((e = getNode(hash(key), key)) != null) {
V oldValue = e.value;
e.value = value;
afterNodeAccess(e);
return oldValue;
}
return null;
}
六、查(get*)
1、get
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
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) {
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;
}
/**
* 树的查找方式
**/
final TreeNode<K,V> getTreeNode(int h, Object k) {
return ((parent != null) ? root() : this).find(h, k, null);
}
final TreeNode<K,V> find(int h, Object k, Class<?> kc) {
TreeNode<K,V> p = this;
do {
int ph, dir; K pk;
TreeNode<K,V> pl = p.left, pr = p.right, q;
if ((ph = p.hash) > h)
p = pl;
else if (ph < h)
p = pr;
else if ((pk = p.key) == k || (k != null && k.equals(pk)))
return p;
else if (pl == null)
p = pr;
else if (pr == null)
p = pl;
else if ((kc != null ||
(kc = comparableClassFor(k)) != null) &&
(dir = compareComparables(kc, k, pk)) != 0)
p = (dir < 0) ? pl : pr;
// 开始递归起来了
else if ((q = pr.find(h, k, kc)) != null)
return q;
else
p = pl;
} while (p != null);
return null;
}
3、getOrDefault
这个方法还是能被提及一下的,特别是 map.getOrDefault(k, new ArrayList())
的时候。先查一遍,为 null 就返回默认值。
@Override
public V getOrDefault(Object key, V defaultValue) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? defaultValue : e.value;
}
七、链表与数据之间的转换
我们都知道当 HashMap 的链表达到一定的条件的时候会转化为红黑树,但是这里的条件是属于哪种场景的,还有没有其他场景,存在转换关系?
链表转化为红黑树就一种,resize 的时候判断节点数量与链表是否达到转化的阈值,而将红黑树转化为链表却又是另外的一套逻辑。移除节点与 resize 的 split 都会有可能将树转化为链表。
final void removeTreeNode(HashMap<K,V> map, Node<K,V>[] tab,
boolean movable) {
...
if (root == null
|| (movable
&& (root.right == null
|| (rl = root.left) == null
|| rl.left == null))) {
tab[index] = first.untreeify(map); // too small
return;
}
...
}
// 将红黑树转化为链表的方法
final Node<K,V> untreeify(HashMap<K,V> map) {
Node<K,V> hd = null, tl = null;
for (Node<K,V> q = this; q != null; q = q.next) {
Node<K,V> p = map.replacementNode(q, null);
if (tl == null)
hd = p;
else
tl.next = p;
tl = p;
}
return hd;
}
上方的代码中表达了红黑树转化为链表的条件,当树的根节点root、root.left、root.right、root.left.left 中任意一个为 null 的时候都会触发转化为链表的逻辑。
也就是说当节点有四个的时候,移除掉任何一个节点都会触发转化的逻辑,而当节点为 3 - 10 个之间,如果 remove 了这四个节点中的任意一个节点,同样会在下一次 remove 中将树转化为链表。
八、总结
总的来说要注意的点的数量放在整个 HashMap 方法中占比还是不算特别大的,需要重点看的还是他的 初始化的时候(二的幂次方)
、扩容的时候(达到阈值就要扩容了,new 一个Node数组,迁移数据)、树转化成链表的时候
,整体还是不难的,也是很容易理解的,除了红黑树的添加那部分,查询反而是很简单了。
看完代码也就知道了,不安全、尾插法、容量是2的幂次方这些东西是怎么来了的。