类的初始化顺序
1、成员变量
//初始默认容量16,是2的倍数
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4 ;
//最大容量,如果两个构造函数中的任何一个带参数地隐式指定了更高的值,则使用最大容量。必须是2的幂<=1<<30。
static final int MAXIMUM_CAPACITY = 1 << 30;
//负载因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//树化阈值,达到8个节点数量,就转为树
static final int TREEIFY_THRESHOLD = 8;
//非树化阈值,节点数量变为6时,转为链表
static final int UNTREEIFY_THRESHOLD = 6;
//最小树化数组容量,即当数组容量达到64以上才会树化
static final int MIN_TREEIFY_CAPACITY = 64;
//------------------------------------------------------------
//存储数据的节点数组
transient Node<K,V>[] table;
//entry集合
transient Set<Entry<K,V>> entrySet;
//元素数量
transient int size;
//修改次数,用于fastFail机制
transient int modCount;
//扩容阈值
int threshold;
//负载因子,可以自定义初始化
final float loadFactor;
2、构造方法
构造方法就是如下四种,主要就是通过传入的参数赋值给负载因子loadFactory和扩容阈值threshold

这里比较特殊一点的就是第四个方法,传入了一个map,具体实现如下
public HashMap(Map<? extends K,? extends V> map){
//1、给负载因子赋值,默认0.75
this.loadFactor = DEFAULT_LOAD_FACTOR;
//2、将参数map的值给放进新构造的map中
putMapEntries(map, false);
}
// Implements Map.putAll and Map constructor.
// 用于构造 和 putAll方法
final void putMapEntries(Map<? extends K,? extends V> map,boolean evict){
int size = map.size();
if(size > 0){
//当参数map中有值时才进行
if(table == null){
//table为null,即构造逻辑分支,计算map数量的扩容阈值
float ft = ((float) size /loadFactor) + 1.0f;
int t = (ft < (float) MAXIMUM_CAPACITY) ? (int) ft : MAXIMUM_CAPACITY;
if(t > threshold){
threshold = tableSizeFor(t);
}
}else if(size > threshold){
//map数量超过扩容阈值,需要进行扩容后再进行保存map的值
resize();
}
for (Entry<? extends K, ? extends V> entry : map.entrySet()) {
K key = entry.getKey();
V value = entry.getValue();
putVal(hash(key),key,value,false,evict);
}
}
}
正常来说,size > threshold * loadFactory 就要扩容了,在构造逻辑判断分支里面,这里的threshold可能是没有值,就不能拿来这样计算,所以这里反过来用参数map的size来反计算出threshold的值,用于构造。
这里还有个比较有趣的方法tableSizeFor(),我们都知道,map的容量都是2的次幂的数,但是由于传入的值不一定刚好就需要计算,而这里的计算逻辑还是比较有参考意义。
/**
* 找出大于给定capacity的最小2的幂的数
* 3->4,5->8,11->16
* 原理:因为任意数的二进制最高位数都是1,要求比他大的最小2的幂的数
* 只需要让最高有效位的前一位变为1,后面的低位变为0,就是需要的值
* case:c = 6
* c:0110
* c | c >>> 1 -> 0110 | 0011 -> 0111 这就得出全是1的值了,再加1就是1000了,就是8就是我们需要的值
* 因为int是2个字节,有16个bit,所以为了让最高位及其一下的值都变为1,需要最高右移16位
*/
static final int tableSizeFor(int capacity){
int n = capacity - 1;//减一是为了避免初始就是2的幂的情况,以免算出的来的值比应该算的大一倍
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;
}
举个例子就很清楚明白了
假设 capacity = 17 ,比17大的最小2的次幂数是32 换成2进制也就是 100000
n = 17 - 1 = 16; 换成2进制就是 10000
第一次:n |= n >>> 1;
10000
01000
---------
11000
第二次:n |= n >>> 2;
11000
00110
---------
11110
第三次:n |= n >>> 4;
11110
00001
---------
11111
其实到这已经ok了
第四次,第五次都是:值已经不变了,所有位都变为了1
11111
00000
---------
11111
最后n+1 = 32(100000 二进制)就求出来了
回到原来的putMapEntries()方法,在对初始字段计算并赋值后,就是遍历把参数map的k-v添加进来。
3、主要方法
3.1、putVal()方法
当通过map进行初始化时,就需要把参数map中的一个个k-v值写进初始化的map中,也就是这里的putVal方法,这里我们再来分析这个方法。
final V putVal(int hash,K key,V value,boolean onlyIfAbsent,boolean evict){
//
Node<K,V>[] tab = table;
int length ;
if(tab == null || (length = tab.length) == 0){
//1、当tab为空的时候,进行初始化
tab = resize();
length = tab.length;
}
//2、计算要存储的节点重新hash散列的索引下标,存储(就是放到哪一个数组块块中)
int reHashIndex = (length - 1) & hash;
Node<K,V> node = tab[reHashIndex];
if(node == null){
//2.1、如果重新散列的下标处没有节点,即该位置第一个节点,直接赋值放到数组中
tab[reHashIndex] = newNode(hash,key,value,null);
}else {
//2.2、如果该节点有值,则分不同情况放到链表中
Node<K,V> existNode;
if(node.hash ==hash &&
(node.getKey() == key || (key != null && key.equals(node.getKey())))){
existNode = node;
}else if(node instanceof TreeNode){
//2.2.1、如果是树节点,用TreeNode.putTreeVal方法进行保存
existNode =((TreeNode<K,V>)node).putTreeVal(this, tab, hash, key, value);
}else {
//2.2.2、如果是链表就遍历链表,添加节点
// 是链表节点的话,如果新插入的节点是第9个节点,才对链表进行转化为红黑树
for (int binCount = 0 ; ;++binCount){
if((existNode = node.next) == null){
//如果是最后一个节点了,则在最末尾添加进去
node.next = newNode(hash,key,value,null);
if(binCount >= TREEIFY_THRESHOLD - 1){
//如果达到树化的标准,即链表原数量达到8后
treeifyBin(tab,hash);
}
break;
}
if (existNode.hash == hash &&
((key = existNode.key) == key || (key != null && key.equals(existNode.key)))){
//如果发现需要存储的key已经在链表中,则找到该结点
break;
}
//将节点往链表后面移动
node = existNode;
}
}
//通过上述步骤找到该key在map中的位置
if(existNode != null){
//如果发现该key已经存在在map中,则直接更新值,没在map中的情况已经通过新增的方式在前面步骤中添加进去了
V oldValue = existNode.value;
if(!onlyIfAbsent || oldValue == null){
//并非根据仅当缺席的时候才更新,或者值为null更新
existNode.value=value;
}
afterNodeAccess(existNode);
return oldValue;
}
}
//修改次数加一
modCount++;
if(++size > threshold){
//数量超过阈值,进行扩容
resize();
}
afterNodeInsertion(evict);
return null;
}
简略说一下大概逻辑
1、初始化数组
2、计算hash在数组中的索引下标
3、判断该位置是否有节点,无值直接复制放进去
4、有值判断是否是树节点,是树节点则添加树中
5、如果是链表,判断原链表数量是否达到8,有8个则转为红黑树,再添加新节点
6、没有达到树化阈值,则直接加在链表末尾
7、在上述步骤中,如果发现该key对应节点已存在,则是在最后进行新值替换
8、最后检查map的size是否超过扩容阈值,判断是否进行扩容
3.2、treeifyBin()方法
在上面的putVal方法中知道,当链表数量达到8时再插入节点就会进行树化(链表转红黑树),就是该方法,源码及注释分析如下:
/**
* 尝试将链表转为树
*/
final void treeifyBin(Node<K,V>[] tab, int hash){
int length ,index;
Node<K,V> e ;
if(tab == null || (length = tab.length) < MIN_TREEIFY_CAPACITY){
//1、当数组容量小于64时,不转红黑树,只进行扩容
resize();
}else if((e = tab[index = (length - 1) & hash]) != null){
//2、该hash的数组索引处头节点开始
TreeNode<K,V> head = null, tail = null;
do {
//将链表转为树节点,将所有树节点连在一起
TreeNode<K,V> p = replacementTreeNode(e, null);
if(tail == null){
//如果没有尾节点,就直接赋值head
head = p;
}else {
//尾节点存在,将树节点加在tail的后面,
//树节点,指明前后节点(左右节点)
p.prev = tail;
tail.next = p;
}
tail = p;
}while ((e = e.next) != null);
if((tab[index] = head) != null){
//如果头节点存在,则进行红黑树转换
//前面的树节点只是连起来了,还没做平衡处理
head.treeify(tab);
}
}
}
通过源码发现,在满足了最开始的链表数量要求后,还有对数组长度的限制,当数组长度没有达到64时,只对数组进行扩容,不进行树化操作。
基本逻辑就是:
1、判断数组长度是否达到64,没有即进行扩容
2、遍历链表,将链表节点转为树节点连起来
3、平衡树节点,形成红黑树(具体后面再细细分析)
3.3、resize()方法
从上面的putVal和treeifyBin方法中都看到了resize方法,该方法的作用就是数组进行扩容,以及相应的数据处理。
/**
* 扩容方法
*/
final Node<K,V>[] resize(){
Node<K,V>[] oldTable = table;
int oldCapacity = oldTable == null ? 0 : oldTable.length;
int oldThreshold = threshold;
//根据旧map的容量和阈值计算新的容量和阈值
int newCapacity, newThreshold = 0;
if(oldCapacity > 0){
//1、旧map容量大于0.说明已经初始化扩容过
if(oldCapacity >= MAXIMUM_CAPACITY){
//1.1、如果扩容前map容量已经大于等于最大值,则最大阈值就是maxValue,不再扩容
threshold = Integer.MAX_VALUE;
return oldTable;
}else if((newCapacity = oldCapacity << 1) < MAXIMUM_CAPACITY
&& oldCapacity >= DEFAULT_INITIAL_CAPACITY){
//1.2、扩容后,容量扩大一倍后仍小于最大容量,且扩容前容量不超过16时,把扩容阈值也直接扩大一倍
//这里判断旧map容量要大于等于16,是因为初始化的容量为16.阈值threshold为16 * 0.75 = 12,不能直接扩大两倍来算
newThreshold = oldThreshold << 2;
}
}else if(oldThreshold > 0){
//2、map容量为0,阈值大于0,即初始化时指定的容量,
// 当map中无值且扩容阈值大于0时,新的初始化容量用扩容阈值代替
newCapacity = oldThreshold;
}else {
//当map中无值且扩容阈值也等于0时,默认初始容量为16.默认初始扩容阈值为12 = 16 * 0.75
newCapacity = DEFAULT_INITIAL_CAPACITY;
newThreshold = (int) (DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR);
}
if(newThreshold == 0){
//如果计算出来的扩容阈值为0,则用容量 * 负载因子 计算扩容阈值
float ft = (float) newCapacity * loadFactor;
newThreshold = (newCapacity < MAXIMUM_CAPACITY && ft < (float) MAXIMUM_CAPACITY ? (int) ft : Integer.MAX_VALUE);
}
threshold = newThreshold;
//初始化数组
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTable = (Node<K, V>[]) new Node[newCapacity];
table = newTable;
if(oldTable != null){
//3、旧map中数组不为空时,map数据迁移
for (int j = 0; j < oldCapacity;j++){
Node<K,V> e ;
if((e = oldTable[j]) != null){
oldTable[j] = null;
if(e.next == null){
//3.1、如果该数组节点下无链表节点,即单个元素节点,
// 则重新散列计算该节点的数组下标位置,进行保存
newTable[e.hash & (newCapacity - 1)] = e;
}else if(e instanceof TreeNode){
//3.2、如果是树节点,则进行拆分
((HashMap.TreeNode<K,V>)e).split(this,newTable,j,oldCapacity);
}else {
//3.3、如果是链表节点,则拆分成两个链表,分别放在高位索引和低位索引
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 & oldCapacity) == 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;
newTable[j] = loHead;
}
if(hiTail != null){
hiTail.next = null;
//因为容量扩大了一倍,即使 j + oldCapacity 也不会超出范围
newTable[j + oldCapacity] = hiHead;
}
}
}
}
}
return newTable;
}
具体每个步骤分析注释可以看源码,不一一赘述,大概说一下就是:
1、计算新的数组容量capacity和扩容阈值threshold,然后重新new一个新容量的数组,
2、把数组中的单节点重新计算数组下标存储,
3、如果是树节点则进行拆分保存,
4、链表节点拆分成两个新的链表,分别放在新table中的低位索引和高位索引
以上就是大概构造hashmap的相关源码情况解析,后面有空再细说一下树节点转为红黑树的情况
PS:记录纯属学习,有问题欢迎指出!

266

被折叠的 条评论
为什么被折叠?



