java8中HashMap扩容机制-结点的挂载
Posted You295
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了java8中HashMap扩容机制-结点的挂载相关的知识,希望对你有一定的参考价值。
java8中HashMap扩容机制-结点的挂载
HashMap简介+成员变量
1.HashMap在底层数据结构上采用的是 :数组+链表+红黑树,通过散列映射储存键值对数 据,故查询时访问速度比较快;HashMap最多允许一对键值对的key值为null,允许多对键值对的value为null;排列无序,非线程安全。
2.Node<K,V>:链表的节点,包含了key,value,hash,next 四个元素。
3. size:记录元素的个数。
4.table:Node<K,v>类型的数组,里面元素为链表,储存元素。
5.LoadFactor:负载因子(0.75)。
6.threshold:阀值,决定HashMap在什么时候进行扩容,以及扩容后的大小。阀值=容量*负载因子
put方法
自己实现put方法代码,,仅仅适用于无扩容情况时,如下:
/**
* 添加数据
*/
@Override
public void put(K key, V value) {
int index = (key == null) ? 0 : key.hashCode() % table.length;// 数组的索引
Entry<K, V> data = new Entry<>(key, value);
Node<K, V> newNode = new Node<>(data, null);
if (table[index] == null) { // 空链表无节点
table[index] = newNode;
} else {
Node<K, V> tmp = table[index];
while (tmp != null && tmp.next != null) { // 尾插法
if (tmp.data.getKey() == key || tmp.data.getKey().equals(key)) {
tmp.data.setValue(value);
return;
}
tmp=tmp.next;
}
if (tmp.data.getKey() == key || tmp.data.getKey().equals(key)) {
tmp.data.setValue(value);
return;
}
tmp.next = newNode;
}
this.size++; // 容量大小+1
}
源码如下:
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
Node<K, V>[] tab;//哈希表数组
Node<K, V> p;//桶位置的头结点
int n, i; //n:哈希表数组大小,,i:下标
//当哈希表数组为null或者长度为0时,初始化哈希表数组
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<K, V> 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<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);
//如果当前链表中元素大于数化的阀值,将链表转变为数
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
//如果key-非头节点,已经存在,直接结束循环
if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;//重置p,用于遍历
}
}
//如果key已经存在则更新value值
if (e != null) { // existing mapping for key
V oldValue = e.value;
//更新当前的value值
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
//如果键对个数大于阀值时,调用resize()方法,
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
}
在源码putVal方法中第47和48 行代码可以看出,当添加完元素以后,如果HashMap发现size大于threshold(阀值)时,则会调用resize()方法进行扩容
java8中HashMap扩容需要满足的条件:当前数据存储的数量(size)大小必须大于等于阀值;
HashMap在存值时默认容量大小为16,,负载因子为0.75,,阀值为12。
扩容机制-resize方法
HashMap在三种情况下扩容:
1.使用默认构造方法初始化HashMap。从源码中可以看出HashMap在刚开始初始化的时候会返回一个空table,threshold值为0,所以第一次发生扩容的容量就是为默认值16,,并且threshold(阀值)为12=16*0.75
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
2.指定初始容量的构造方法初始化HashMap。源码中可以得到初始容量等于阀值。阀值=当前容量(threshold)*负载因子
3.HashMap不是第一次扩容,如已经扩容过了,那么每次table的容量以及threshold为原来的两倍。
扩容与插入元素的顺序: HashMap初始化后首次插入数据时,先resize扩容在插入数据,,之后每次插入数据的个数达到阀值的时候再扩容,这时则为先插入数据再扩容。
resize方法 源码如下:
Node<K, V>[] oldTab = table; // 记录原来的table
// oldCap:原来数组的长度,,oldThr:原来table中节点个数的阀值
// 如果当前数组等于null,长度返回0,否则返回当前数组长度
int oldCap = (oldTab == null) ? 0 : oldTab.length;
// 当前的阀值,默认为12
int oldThr = threshold;
int newCap, newThr = 0;
if (oldCap > 0) {
// 数组不能变大,则调大threshold,计算扩容后的大小
if (oldCap >= MAXIMUM_CAPACITY) {
// 如果超过了Int最大值就不在扩充了
threshold = Integer.MAX_VALUE;
return oldTab;
}
// 没有超过最大值,就扩容为原来的两倍
// 1.(newCap = oldCap << 1) < MAXIMUM_CAPACITY 扩大两倍以后的容量小于最大容量()阀值
// 2.oldCap >= DEFAULT_INITIAL_CAPACITY 原来数组长度大于等于数组初始化长度16
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold 阀值扩大一倍
}
// 原来阀值点大于0,直接赋值
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;// 老阀值赋值给新的数组长度
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY; // 值为16
newThr = (int) (DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
// 计算新resize的最大上限
if (newThr == 0) {
float ft = (float) newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float) MAXIMUM_CAPACITY ? (int) ft : Integer.MAX_VALUE);
}
// 新的阀值为扩容后的两倍,即为24
threshold = newThr;
// 创建新的哈希表
@SuppressWarnings({ "rawtypes", "unchecked" })
Node<K, V>[] newTab = (Node<K, V>[]) new Node[newCap];// newCap是新的数组长度--32
table = newTab; 新数组被创建出来后,table指向它
// 判断原来数组是否为空
if (oldTab != null) {
// 把每个bucket都移动到新的bucket中去
// 遍历原来哈希表的每一个桶,重新计算桶里元素的新位置
for (int j = 0; j < oldCap; ++j) {
Node<K, V> e;
if ((e = oldTab[j]) != null) {
// 将原来的数据赋值为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;
// 判断如果等于true,e节点在resize之后不需要移动位置
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
// 原索引+oldCap
} else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
// 将原索引放到bucket里
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
// 原索引+oldCap放到bucket里去
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
知识点:
第一次添加元素的时候,初始化默认的长度为16,继续往map中加入元素,通过hash的值和数组的长度来决定放在数组的那个位置,,如果出现放在同一位置时,优先一链表的形式存放,在同一个位置存放的个数达到8个时(>=7),判断是否转变为红黑树;如果数组的长度还小于64,则会发生扩容数组,如果数组的长度大于等于64的时候才会将该结点的链表转变为数。扩容完成之后,如果某个节点的是数,同时该节点的个数小于等于6,则会将该数转变为链表。
以上是关于java8中HashMap扩容机制-结点的挂载的主要内容,如果未能解决你的问题,请参考以下文章