7:hashMap底层解析
hashMap底层代码详细解析:
public static void main(String[] args) {
HashMap map = new HashMap();
map.put("rj","666")
}
hash方法:
我们通过hash方法计算索引,得到数组中保存的位置,看一下源码
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
为什么不直接使用key.hashCode(),而要进行异或操作?我们知道hash的目的是为了得到进行索引,而hash是有可能冲突的,也就是不同的key得到了同样的hash值,这样就很容易产业碰撞,为了减少这种情况的发生运用^
第一次put:
当第一次put初始时会触发putVal()方法,table-桶状容器中是没有值的
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
putVal()方法详解:
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
/**
* 如果当前HashMap的table数组还未定义或者还未初始化其长度,则先通过resize()进行扩容,
* 返回扩容后的数组长度n
*/
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//通过数组长度与hash值做按位与&运算得到对应数组下标,若该位置没有元素,则new Node直接将新元素插入
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
//否则该位置已经有元素了,我们就需要进行一些其他操作
else {
Node<K,V> e; K k;
//如果插入的key和原来的key相同,则替换一下就完事了
if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
e = p;
/**
* 否则key不同的情况下,判断当前Node是否是TreeNode,如果是则执行putTreeVal将新的元素插入
* 到红黑树上。
*/
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
//如果不是TreeNode,则进行链表遍历
else {
for (int binCount = 0; ; ++binCount) {
/**
* 在链表最后一个节点之后并没有找到相同的元素,则进行下面的操作,直接new Node插入,
* 但条件判断有可能转化为红黑树
*/
if ((e = p.next) == null) {
//直接new了一个Node
p.next = newNode(hash, key, value, null);
/**
* TREEIFY_THRESHOLD=8,因为binCount从0开始,也即是链表长度超过8(包含)时,
* 转为红黑树。
*/
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
/**
* 如果在链表的最后一个节点之前找到key值相同的(和上面的判断不冲突,上面是直接通过数组
* 下标判断key值是否相同),则替换
*/
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;
//onlyIfAbsent为true时:当某个位置已经存在元素时不去覆盖
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
//最后判断临界值,是否扩容。
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
当前tab为null时,putVal()方法又会给我们调一个resize()初始化桶状数组的方法
//定义数组 和 节点 n 长度 i
//tab p =null
//n i =0
// 进行resize() 初始化
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
resize()方法作用:初始化桶状数组 和 扩容
//初始化桶状数组方法
//给table定义一个节点
Node<K,V>[] oldTab = table;
//oldCap 旧容量
int oldCap = (oldTab == null) ? 0 : oldTab.length;
//旧阈值
int oldThr = threshold;
//定义新的容量 和 阈值
int newCap, newThr = 0;
//经过一系列判断 旧的容量和阈值 都为 0 (再这里不作详解,直接判断走到这里-想看可以详细看看判断的过程)
//赋值新的容量 =16
// 新的 阈值 为 =12
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY; //DEFAULT_INITIAL_CAPACITY-每个table的数组长度
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);//每个table可放12个键值对
}
赋值-全局临界值
//threshold 再这里是一个全局变量
threshold = newThr;
//初始化新的数组 赋值给全局数组
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
//table 再这里是一个全局变量
table = newTab;
return newTab;
求下标 直接插入
新创建一个节点 p=tab [i] == p = tab[i = (n - 1) & hash]
提炼出来 i = (n - 1) & hash 等价于 i = (n - 1) % hash 结果相同,
为什么用&不用 % ? 因为:2进制算法效率要高于10进制,计算机默认读取的就是 10进制的
//经过hash算法(重写hashcode和equals方法)计算出当前的hash码值,
//p==null 也就是如果当前table中的数组为null
if ((p = tab[i = (n - 1) & hash]) == null)
//就可以将数据直接插入当前桶状数组中
tab[i] = newNode(hash, key, value, null);
//修改的次数 +1
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
第二次put:key相同,value不同
map.put("lingo", "gaofus");
求得hash 102975770
//i = (n - 1) & hash 求得下标是10 位置为空 直接放值
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
//修改的次数 +1
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
第三次put: key相同,value不同
String value=map.put("lingo", "shuai");
求得hash 102975770 此时当前key已经存在,同桶已经有当前key,所以会走下边的方法
//定义新的 节点 key
Node<K,V> e; K k;
//说明 hash 和 key 一样
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
解决: 直接 替换 返回旧值
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue; //此时table中放入的还是我们的原值- value = gaofus
}
第四次put:
演示的是哈希冲撞:key 不一样 hash code 一样 链表存储(上边说到了 链表则是主要为了解决哈希冲突而存在的)
hash=165372725
map.put("ABCDEa123abc", "新生1");
map.put("ABCDFB123abc", "新生2");
和上边套路一样,通过hash算法计算出,hash值选出 应该放入哪个table桶状数组中
经过计算得出:下标是5 第一次把新生1 放到5 第二次判断新生2 也放到5
5下标不为空 ,且两个key 不一样 使用链表式 挂桶存储
源码如下:
else {
for (int binCount = 0; ; ++binCount) {
//第一个数组的next 为空 直接插尾 存储
//需要注意的 链表的长度 和树形化的临界值8 对比
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;
}
}
树形化需要满足俩个条件:1 链表长度大于8 ,2 数组长度>=64
//如果当前tab为null且tab的长度小于 64
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
//还是会走链表 不会进行链表树形化
resize();
**扩容 **把 数组长度 和 阈值 都左移一位 变成 两倍
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
}
把全局的扩容两倍
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
遍历复制:如果桶下面没有节点 直接 计算tab下标位置 进行赋值,扩容赋值元素的时候 位置要么不变 要么 是 原始下标加上你扩容的长度
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;
如果是树节点 就分割 树的底层大家需要 博主会下期更新
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
如果是链表 组装链表 移动链表
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;
}
总结
本文从源码的角度讲解了扩容机制以及存取原理,主要分析了put方法,put方法的核心为hash(),putVal(),resize(),若有不对之处,请批评指正,望共同进步,谢谢!