1. 如何确定元素位置
● 计算元素的hash值
○ e.hashcode mod (table.length-1) = index,在这个过程中,table.length是固定的,如何让这个index呈现出高度不一致性(减少hash冲突),只能从e.hashcode入手,前期提供再hash提高散列度。
static final int hash(Object key) { // 计算key的hash值
int h;
// 1.先拿到key的hashCode值; 2.将hashCode的高16位参与运算
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
○ 另外,模运算消耗还是比较大的,使用位与运算来代替模运算可以提高效率。这个方法非常巧妙,它通过 “(table.length -1) & h” 来得到该对象的索引位置,这个优化是基于以下公式:x mod 2^n = x & (2^n - 1)。我们知道 HashMap 底层数组的长度总是 2 的 n 次方,并且取模运算为 “h mod table.length”,对应上面的公式,可以得到该运算等同于“h & (table.length - 1)”。这是 HashMap 在速度上的优化,因为 & 比 % 具有更高的效率。
int n = tab.length;
// 将(tab.length - 1) 与 hash值进行&运算
int index = (n - 1) & hash;
● 位运算
1.^(异或运算) ,针对二进制,相同的为0,不同的为1
2 =======>0010
3 =======>0011
2^3就为0001,结果就是1
2.&(与运算) 针对二进制,只要有一个为0,就为0,都为1就为1
2 =======>0010
3 =======>0011
2^3就为0010,结果就是2
3.<<(向左位移) 针对二进制,转换成二进制后向左移动,后面用0补齐
2 的二进制是0010,向左移动3位为10000,10000转换为十进制为16.
4.>>(向右位移) 针对二进制,转换成二进制后向右移动
2 的二进制是0010,向右移动3位为0000
5.>>>(无符号右移) 无符号右移,忽略符号位,空位都以0补齐
10进制转二进制的时候,因为二进制数一般分8位、 16位、32位以及64位 表示一个十进制数,所以在转换过程中,最高位会补零。
其中>>>与>>唯一的不同是它无论原来的最左边是什么数,统统都用0填充。
——比如,byte是8位的,-1表示为byte型是11111111(补码表示法)
b>>>4就是无符号右移4位,即00001111,这样结果就是15。
正数做>>>运算的时候和>>是一样的。区别在于负数运算
那么对于 int index = (n - 1) & hash 就很好解释了,n位2^n ,n-1即让二进制位低位都为1,再做与运算,那么就可以让hashcode的二进制得到充分发挥(如果n-1的二进制地位为0,那么hash二进制低位与时,无论该位是1还是0,运算结果都是0),保证散列。
2.get 方法
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;
// 1.对table进行校验:table不为空 && table长度大于0 &&
// table索引位置(使用table.length - 1和hash值进行位与运算)的节点不为空
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
// 2.检查first节点的hash值和key是否和入参的一样,如果一样则first即为目标节点,直接返回first节点
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
// 3.如果first不是目标节点,并且first的next节点不为空则继续遍历
if ((e = first.next) != null) {
if (first instanceof TreeNode)
// 4.如果是红黑树节点,则调用红黑树的查找目标节点方法getTreeNode
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
do {
// 5.执行链表节点的查找,向下遍历链表, 直至找到节点的key和入参的key相等时,返回该节点
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
// 6.找不到符合的返回空
return null;
}
4.如果是红黑树节点,则调用红黑树的查找目标节点方法 getTreeNode。
final TreeNode<K,V> getTreeNode(int h, Object k) {
// 1.首先找到红黑树的根节点;2.使用根节点调用find方法
return ((parent != null) ? root() : this).find(h, k, null);
}
3.put 方法
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
// 1.校验table是否为空或者length等于0,如果是则调用resize方法进行初始化
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// 2.通过hash值计算索引位置,将该索引位置的头节点赋值给p,如果p为空则直接在该索引位置新增一个节点即可
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);//如果当前位置没有元素,则直接放在当前位置(tab[i]),不为空,继续往下
else {
// table表该索引位置不为空,则进行查找
Node<K,V> e; K k;
// 3.判断p节点的key和hash值是否跟传入的相等,如果相等, 则p节点即为要查找的目标节点,将p节点赋值给e节点
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
// 4.判断p节点是否为TreeNode, 如果是则调用红黑树的putTreeVal方法查找目标节点
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
// 5.走到这代表p节点为普通链表节点,则调用普通的链表方法进行查找,使用binCount统计链表的节点数
for (int binCount = 0; ; ++binCount) {
// 6.如果p的next节点为空时,则代表找不到目标节点,则新增一个节点并插入链表尾部
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
// 7.校验节点数是否超过8个,如果超过则调用treeifyBin方法将链表节点转为红黑树节点,
// 减一是因为循环是从p节点的下一个节点开始的
if (binCount >= TREEIFY_THRESHOLD - 1)
treeifyBin(tab, hash);
break;
}
// 8.如果e节点存在hash值和key值都与传入的相同,则e节点即为目标节点,跳出循环
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e; // 将p指向下一个节点
}
}
// 9.如果e节点不为空,则代表目标节点存在,使用传入的value覆盖该节点的value,并返回oldValue
if (e != null) {
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e); // 用于LinkedHashMap
return oldValue;
}
}
++modCount;
// 10.如果插入节点后节点数超过阈值,则调用resize方法进行扩容
if (++size > threshold)
resize();
afterNodeInsertion(evict); // 用于LinkedHashMap
return null;
}
1.校验 table 是否为空或者 length 等于0,如果是则调用 resize 方法进行初始化,见resize方法详解。
4.如果 p 节点不是目标节点,则判断 p 节点是否为 TreeNode,如果是则调用红黑树的 putTreeVal 方法查找目标节点,见代码块4详解。
7.校验节点数是否超过 8 个,如果超过则调用 treeifyBin方法 将链表节点转为红黑树节点,见代码块6详解。