1.获取 index,有关键的以下两步
参考文档:https://blog.csdn.net/supercmd/article/details/100042302
1.1 扰动函数
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
解读:
- 为什么要用扰动函数?
答:扰动函数就是解决碰撞问题。若不使用扰动函数,则直接将key.hashCode()和下面的步骤2做与运算,则会有以下情景。
以初始长度16为例,16-1=15。2进制表示是00000000 00000000 00001111。和某散列值做“与”操作如下,结果就是截取了最低的四位值。
10100101 11000100 00100101
& 00000000 00000000 00001111
------------------------------
00000000 00000000 00000101
这样就算散列值分布再松散,要是只取最后几位的话,碰撞也会很严重。如果散列本身做得不好,分布上成等差数列的漏洞,恰好使最后几个低位呈现规律性重复,则碰撞会更严重。
- 扰动函数是怎么实现的?
由扰动函数源码可知,会有以下步骤:
①使用key.hashCode()计算hash值并赋值给变量h;
②将h向右移动16位;
③将变量h和向右移16位的h做异或运算(二进制位相同为0,不同为1)。此时得到经过扰动函数后的hash值。图解如下:
h = hashCode(): 1111 1111 1111 1111 1111 0000 1110 1010
-----------------------------------------------------------
h: 1111 1111 1111 1111 1111 0000 1110 1010
h>>>16: 0000 0000 0000 0000 1111 1111 1111 1111
hash = h^(h>>>16): 1111 1111 1111 1111 0000 1111 0001 0101 高16位和低16位 异或运算
-----------------------------------------------------------
(n-1)&hash: 0000 0000 0000 0000 0000 0000 0000 1111 与运算
1111 1111 1111 1111 0000 1111 0001 0101
-------------------------------------------------------------
0000 0000 0000 0000 0000 0000 0000 0101 = 5
- 为什么要将key.hashCode()右移16位?
右移16位正好为32bit的一半,自己的高半区和低半区做异或,是为了混合原始哈希吗的高位和低位,来加大低位的随机性。而且混合后的低位掺杂了高位的部分特征,使高位的信息也被保留下来
1.2 与运算
int index = h & (length - 1); //确定index数组下标
- 为什么要使用与运算?
若直接使用key.hashCode()计算出hash值,则范围为:-2147483648到2147483648,大约40亿的映射空间。若映射得比较均匀,是很难出现碰撞的。但是这么大范围无法放入内存中,况且HashMap的 初始容量为16。所以必须要进行与运算取模。
1.3 如何保证HashMap的容量是2的倍数
/**
* Returns a power of two size for the given target capacity.
* 返回给定目标容量大小的2次方。
*/
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;
}
之所以在开始移位前先将容量-1,是为了避免给定容量已经是8,16这样2的幂时,不减一直接移位会导致得到的结果比预期大。比如预期16得到应该是16,直接移位的话会得到32。
参考文档:https://www.cnblogs.com/xiyixiaodao/p/14483876.html
2.HashMap底层数据结构
- JDK1.8之前的HashMap底层数据结构
- 数组+链表
- JDK1.8之后的HashMap底层数据结构
- 数组+链表+红黑树
- 链表长度>=8 && 数组长度>=64 链表转化为红黑树
- 链表长度>=8 && 数组长度< 64 扩容,原来数组长度*2
- 红黑树节点<=6,自动转化为链表
HashMap 的长度为什么是2的幂次方
为了加快哈希计算以及减少哈希冲突
- 为什么可以加快计算?
我们都知道为了找到 KEY 的位置在哈希表的哪个槽里面,需要计算 hash(KEY) % 数组长度
但是!% 计算比 & 慢很多
所以用 & 代替 %,为了保证 & 的计算结果等于 % 的结果需要把 length 减 1
hash%length==hash&(length-1)的前提是 length 是2的 n 次方
- 为什么可以减少冲突?
假设现在数组的长度 length 可能是偶数也可能是奇数
length 为偶数时,length-1 为奇数,奇数的二进制最后一位是 1,这样便保证了 hash &(length-1) 的最后一位可能为 0,也可能为 1(这取决于 h 的值),即 & 运算后的结果可能为偶数,也可能为奇数,这样便可以保证散列的均匀性。
而如果 length 为奇数的话,很明显 length-1 为偶数,它的最后一位是 0,这样 hash & (length-1) 的最后一位肯定为 0,即只能为偶数,这样任何 hash 值都只会被散列到数组的偶数下标位置上,这便浪费了近一半的空间
因此,length 取 2 的整数次幂,是为了使不同 hash 值发生碰撞的概率较小,这样就能使元素在哈希表中均匀地散列。
解释:
为了能让 HashMap 存取高效,尽量较少碰撞,也就是要尽量把数据分配均匀。我们上面也讲到了过了,Hash 值的范围值-2147483648到2147483647,前后加起来大概40亿的映射空间,只要哈希函数映射得比较均匀松散,一般应用是很难出现碰撞的。但问题是一个40亿长度的数组,内存是放不下的。所以这个散列值是不能直接拿来用的。用之前还要先做对数组的长度取模运算,得到的余数才能用来要存放的位置也就是对应的数组下标。这个数组下标的计算方法是“ (n - 1) & hash
”。(n代表数组长度)
HashMap的put方法
public class HashMap extends AbstractMap
implements Map, Cloneable, Serializable {
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 默认 初始化数组大小 16
static final int MAXIMUM_CAPACITY = 1 << 30; // 数组的最大容量 2的30次方
static final float DEFAULT_LOAD_FACTOR = 0.75f; //扩容因子,加载因子 默认 0.75
static final int TREEIFY_THRESHOLD = 8; //当链表长度>=8,转换为红黑树 (还有个前提,数组长度>=64)
static final int UNTREEIFY_THRESHOLD = 6; //当红黑树长度 小于该值,自动转换为链表
static final int MIN_TREEIFY_CAPACITY = 64; //转换为红黑树时,的数组长度 要大于该值
transient Node[] table;
transient Set> entrySet;
transient int size;
transient int modCount;
int threshold;
final float loadFactor;
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[] tab; Node p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)
e = ((TreeNode)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);
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;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
}
HashMap 的resize方法
final Node[] resize() {
Node[] 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 { // 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[] newTab = (Node[])new Node[newCap];
table = newTab;
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node 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)e).split(this, newTab, j, oldCap);
else { // preserve order
Node loHead = null, loTail = null;
Node hiHead = null, hiTail = null;
Node next;
/*
这里如果判断成立,那么该元素的地址在新的数组中就不会改变。
因为oldCap的最高位的1,在e.hash对应的位上为0,所以扩容后得到的地址是一样的,位置不会改变 ,在后面的代码的执行中会放到loHead中去,最后赋值给newTab[j];
如果判断不成立,那么该元素的地址变为 原下标位置+oldCap,也就是lodCap最高位的1,在e.hash对应的位置上也为1,所以扩容后的地址改变了,在后面的代码中会放到hiHead中,最后赋值给newTab[j + oldCap]
举个例子来说一下上面的两种情况:
设:oldCap=16 二进制为:0001 0000
oldCap-1=15 二进制为:0000 1111
e1.hash=10 二进制为:0000 1010
e2.hash=26 二进制为:0101 1010
e1在扩容前的位置为:e1.hash & oldCap-1 结果为:0000 1010
e2在扩容前的位置为:e2.hash & oldCap-1 结果为:0000 1010
结果相同,所以e1和e2在扩容前在同一个链表上,这是扩容之前的状态。
现在扩容后,需要重新计算元素的位置,在扩容前的链表中计算地址的方式为e.hash & oldCap-1
那么在扩容后应该也这么计算,扩容后的容量为oldCap*2=32,2^5, 二进制为:0010 0000。 所以 newCap=32,
新的计算方式应该为
e1.hash & newCap-1
即:0000 1010 & 0001 1111
结果为0000 1010与扩容前的位置完全一样。
e2.hash & newCap-1 即:0101 1010 & 0001 1111
结果为0001 1010,为扩容前位置+oldCap。
而这里却没有e.hash & newCap-1 而是 e.hash & oldCap,其实这两个是等效的,都是判断倒数第五位是0,还是1。
如果是0,则位置不变,是1则位置改变为扩容前位置+oldCap。
再来分析下loTail, loHead这两个的执行过程(假设(e.hash & oldCap) == 0成立):
第一次执行:
e指向oldTab[j]所指向的node对象,即e指向该位置上链表的第一个元素.
loTail为空,所以loHead指向与e相同的node对象(loHead = e;),然后loTail也指向了同一个node对象(loTail = e;)。
最后,在判断条件e指向next,就是指向oldTab链表中的第二个元素
第二次执行:
lotail不为null,所以lotail.next指向e,这里其实是lotail指向的node对象的next指向e,
也可以说是,loHead的next指向了e,就是指向了oldTab链表中第二个元素。此时loHead指向
的node变成了一个长度为2的链表。然后lotail=e也就是指向了链表中第二个元素的地址。
第三次执行:
与第二次执行类似,loHead上的链表长度变为3,又增加了一个node,loTail指向新增的node......
hiTail与hiHead的执行过程与以上相同。
由此可以看出,loHead是用来保存新链表上的头元素的,loTail是用来保存尾元素的,直到遍历完链表。
这是(e.hash & oldCap) == 0成立的时候。
(e.hash & oldCap) == 0不成立的情况也相同,其实就是把oldCap遍历成两个新的链表,
通过loHead和hiHead来保存链表的头结点,然后将两个头结点放到newTab[j]与newTab[j+oldCap]上面去。
*/
// 原文链接:https://blog.csdn.net/dingjianmin/article/details/109247278
do {
next = e.next;
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e; // 相当于 loTail指向的Node对象的next 指向了 e
loTail = 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;
}
HashMap 中的红黑树
static final class TreeNode extends LinkedHashMap.LinkedHashMapEntry {
TreeNode parent; // red-black tree links
TreeNode left;
TreeNode right;
TreeNode prev; // needed to unlink next upon deletion
boolean red;
TreeNode(int hash, K key, V val, Node next) {
super(hash, key, val, next);
}
final TreeNode root() {
for (TreeNode r = this, p;;) {
if ((p = r.parent) == null)
return r;
r = p;
}
}
/**
* Ensures that the given root is the first node of its bin.
*/
static void moveRootToFront(Node[] tab, TreeNode root) {}
//找出对应的红黑树的节点:查找数据
final TreeNode find(int h, Object k, Class> kc) {}
final TreeNode getTreeNode(int h, Object k) {
return ((parent != null) ? root() : this).find(h, k, null);
}
//树化 Node--》TreeNode
final void treeify(Node[] tab) {}
//去树化 TreeNode--》》Node
final Node untreeify(HashMap map) {}
//插入数据
final TreeNode putTreeVal(HashMap map, Node[] tab,
int h, K k, V v) {}
//删除数据
final void removeTreeNode(HashMap map, Node[] tab,
boolean movable) {}
//rehash :扩容时,将旧数据rehash,重新确认index;将树分为高区和低区两个数
final void split(HashMap map, Node[] tab, int index, int bit) {}
//左旋
static TreeNode rotateLeft(TreeNode root,
TreeNode p){}
//右旋
static TreeNode rotateRight(TreeNode root, TreeNode p){}
//插入数据后,对红黑树结构进行自平衡
static TreeNode balanceInsertion(TreeNode root, TreeNode x)
//删除数据后,对红黑树结构进行自平衡
static TreeNode balanceDeletion(TreeNode root,
TreeNode x)
//递归检查,是否符合红黑树规则
static boolean checkInvariants(TreeNode t) {}
}
参考文档:https://blog.csdn.net/qq_28063811/article/details/102842862
补充关于红黑树的知识:
参考文档:https://www.jianshu.com/p/e136ec79235c
红黑树的特性:
1、每个节点要么是红色,要么是黑色,但根节点永远是黑色的;
2、每个红色节点的两个子节点一定都是黑色;
3、红色节点不能连续(也即是,红色节点的孩子和父亲都不能是红色);
4、从任一节点到其子树中每个叶子节点的路径都包含相同数量的黑色节点;
5、所有的叶节点都是是黑色的(注意这里说叶子节点其实是上图中的 NIL 节点);
————————————————
原文链接:https://blog.csdn.net/javageektech/article/details/102385013
红黑树插入情景:
红黑树删除情景
- 红黑树:一种自平衡的二叉查找树
- 怎么做到自平衡: 左旋,右旋,变色
- 什么情况下需要自平衡操作: 插入和删除结点时