一、前言
- 1.什么事hash?
- 个人认为hash概念 就是把任意长度的输入通过一个hash算法之后,映射成固定长度的输出
- 当两个key通过hashCode计算相同时,则发生了hash冲突(碰撞),HashMap解决hash冲突的方式是用链表。
- 2.解决hash冲突
当发生hash冲突时,则将存放在数组中的Entry设置为新值的next(这里要注意的是,比如A和B都hash后都映射到下标i中,之前已经有A了,当map.put(B)时,将B放到下标i中,A则为B的next,所以新值存放在数组中,旧值在新值的链表上)
所以当hash冲突很多时,HashMap退化成链表。
- 3.hashMap常量
//默认的初始容量为16,必须是2的幂次
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
//最大容量即2的30次方
static final int MAXIMUM_CAPACITY = 1 << 30;
//默认加载因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//当put一个元素时,其链表长度达到8时将链表转换为红黑树
static final int TREEIFY_THRESHOLD = 8;
//链表长度小于6时,解散红黑树
static final int UNTREEIFY_THRESHOLD = 6;
//默认的最小的扩容量64,为避免重新扩容冲突,至少为4 * TREEIFY_THRESHOLD=32,即默认初始容量的2倍
static final int MIN_TREEIFY_CAPACITY = 64;
- 4.HashMap 储存结构
jdk1.8来说就是由 数组 + 链表 + 红黑树所构成的。
当你第一次put时候回创建HashMap (散列表懒加载机制)并不是new HashMap就去创建了
每个数据单元都是一个Node结构。 初始长度:16 负载因子 0.75(75%)
// 1. 我们向 HashMap 中所放置的对象实际上是存储在该数组当中;
transient Node<k,v>[] table;//存储(位桶)的数组</k,v>
// 2. 而Map中的key,value则以Entry的形式存放在数组中
static class Entry<K,V> implements Map.Entry<K,V> {
final K key;
V value;
Entry<K,V> next;
int hash;
}
// 3. 而这个Entry应该放在数组的哪一个位置上(这个位置通常称为位桶或者hash桶,即hash值相同的Entry会放在同一位置,用链表相连),是通过key的hashCode来计算的。
final int hash(Object k) {
int h = 0;
h ^= k.hashCode();
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
// 4. 通过hash计算出来的值将会使用indexFor方法找到它应该所在的table下标:
static int indexFor(int h, int length) {
return h & (length-1);//length=2 的整数次幂
}
// 这里哈希值与上(length-1),length=传入的容量是16的话,16-1=15,二进制1111,即对h取低四位,从而对应0-15个位桶
- 5.链表转红黑树
- 当链表长度达到了8
- 当前散列表数组长度已经达到了64
否则就算slot内部链表长度到了8,他也不会链转树。只是会简单的发生一次resize,散列表扩容。
//红黑树
static final class TreeNode<k,v> extends LinkedHashMap.Entry<k,v> {
TreeNode<k,v> parent; // 父节点
TreeNode<k,v> left; //左子树
TreeNode<k,v> right;//右子树
TreeNode<k,v> prev; // needed to unlink next upon deletion
boolean red; //颜色属性
TreeNode(int hash, K key, V val, Node<k,v> next) {
super(hash, key, val, next);
}
//返回当前节点的根节点
final TreeNode<k,v> root() {
for (TreeNode<k,v> r = this, p;;) {
if ((p = r.parent) == null)
return r;
r = p;
}
}
- 6.hash值怎么来的?
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
// 个人理解 要进行从高位到低位的传播(异或),主要还是为了解决hash碰撞问题。
// 虽然HashMap使用拉链法作为hash碰撞的解决方案,但是仍可以在hash函数的设置上去进行一定程度的优化,来减少碰撞的可能性。
- 如果直接使用key.hashCode()作为hash值的话,存在一些问题。
举例说明,HashMap的默认长度为16,并且是通过(table.length - 1) & hash的方式得到key在table中的下标
如果key1.hashCode()=1661580827(二进制为0110,0011,0000,1001,1011,0110,0001,1011),key2.hashCode()=1661711899(二进制为0110,0011,0000,1011,1011,0110,0001,1011)
在与掩码进行与的过程中,只有后4位起作用,导致得到的下标值均为11,导致高位完全失效,加大了冲突的可能性。
- 如果通过高位向低位异或传播的话,高位同样参与到key在table中下标的运算,减少了碰撞的可能性
key1.hashCode() ^ (key1.hashCode() >>>16)=1661588754(二进制为0110,0011,0000,1001,1101,0101,0001,0010)
key2.hashCode() ^ (key2.hashCode() >>>16)=1661719824(二进制为0110,0011,0000,1011,1101,0101,0001,0000)
在于掩码进行与操作得到的下标分别为2和0,减少了冲突的可能性。
- 7.hashMap的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; //判断键值对数组table[i]是否为空或为null,否则执行resize()进行扩容
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length; //根据键值key计算hash值得到插入的数组索引i,如果table[i]==null,直接新建节点添加,否则如果table[i]不为null,看下面注释
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k; //如果table[i]不为null,则判断table[i]的首个元素是否和key一样,如果相同直接覆盖value,否则如果不一样,则看下面注释
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p; //判断table[i]是否为treeNode,即table[i]是否是红黑树,如果是红黑树,则直接在树中插入键值对,否则,看下面注释
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else { //遍历table[i],判断链表长度是否大于8,大于8的话把链表转换为红黑树,在红黑树中执行插入操作,否则进行链表的插入操作,遍历过程中若发现key已经存在直接覆盖value即可。
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;
}
} //key已经存在,将新value替换旧value值具体操作
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount; //插入成功之后,判断实际存在的键值对数量size是否超过了最大容量threshold,如果超过,进行扩容
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
为了更好的理解hashmap如何进行过put操作,可以看下图
- 8.resize方法(扩容机制)
在jdk1.8中,resize方法是在hashmap中的键值对大于阙值时,
初始化时,
链表转红黑树时,
putAll时,就会调用resize()方法进行扩容
final Node<K,V>[] resize() {
//保存旧的 Hash 数组
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;
}
else if (oldThr > 0)
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"})
//创建新的 Hash 表
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
//遍历旧的 Hash 表
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 {
//以链表形式存在的节点;
//这一段就是新优化的地方,见下面分析
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) {
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;
}
等等等。。还有size()、isEmpty()、clear()、containsValue(Object value)、values()等等方法,在这就不一一列举了,大家可以查看JDK1.8 HashMap源码
😁 作者:Teddy (公众号:鸡仓故事汇)
ok!到这里就大功告成,小编(Teddy)在这里先感谢大家的到来。
虽然不是太详细,小编已经很努力,给小编来个一键三连(点赞,关注,收藏),小编会越来越努力。。。