记录读源码-HashMap(1)
HashMa源码很久没看了,有点忘记,还是得做点笔记
1.HashMap中定义的常量
1.默认容量
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
2.最大容量
static final int MAXIMUM_CAPACITY = 1 << 30;
3.装载因子默认值
装载因子用来衡量HashMap满的程度,loadFactor的为0.75f。
static final float DEFAULT_LOAD_FACTOR = 0.75f;
4.由链表转换成树的阈值
由链表转换成树的阈值,当桶中数量超过TREEIFY_THRESHOLD时使用树来代替链表。默认值是8
static final int TREEIFY_THRESHOLD= 8;
5.由树转换成链表的阈值
当执行resize操作时,当桶中数量少于UNTREEIFY_THRESHOLD时使用链表来代替树。默认值是6
static final int UNTREEIFY_THRESHOLD = 6;
6.hash表开始树化阈值
static final int MIN_TREEIFY_CAPACITY = 64;
2.HashMap中定义的变量
transient Node[] table;//节点表,后面统称为(>﹏<)
transient Set>entrySet;
transient int size;// HashMap中存放KV的数量(为链表和树中的KV的总和)
transient int modCount;// 修改的次数, 主要用于迭代的快速失败
int threshold;// threshold表示当HashMap的size大于threshold时会执行resize操作,threshold=capacity*loadFactor
final float loadFactor; // 装载因子用来衡量HashMap满的程度,loadFactor的默认值为0.75f。计算HashMap的实时装载因子的方法为:size/capacity
3. 内部数据结构Node
HashMap采用内部静态类Node来保存键值对, Node类定义如下,其中hash为key的hash值。
4.hash()
static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);}
从上代码可以看到key的hash值的计算方法:将key的hashCode右移16位后,与自己做异或,其实就是低16位与高16位异或作为key的最终hash值,目的是加大低位的随机性,而且混合后的低位掺杂了高位的部分特征,这样高位的信息也被变相保留下来。例如:
5.获取数据方法get
public V get(Object key) { Node e; return (e = getNode(hash(key), key)) == null ? null : e.value;}
get方法调用getNode方法获取对应的value值, getNode方法如下:
查找的核心逻辑是封装在 getNode 方法中,源码还是可以看懂的,我稍微加了注释。第一步是确定桶位置,其实现代码如下:
first = tab[(n - 1) & hash]
这里可以解释HashMap 中桶数组的大小 length 为什么总是2的幂,此时,(n- 1) & hash 等价于对 length 取余。但取余的计算效率没有位运算高,所以(n - 1) & hash也是一个小的优化,例如下图:
5.添加数据put方法
通过前面get方法的分析,大概可以知道插入对象的流程吧,首先肯定是先定位要插入的键值对属于哪个桶,定位到桶后,再判断桶是否为空。如果为空,则将键值对存入即可。如果不为空,则需将键值对接在链表最后一个位置,或者更新键值对。其实,HashMap 的插入流程比上面描述复杂得多,首先是HashMap的扩容,其次是树化过程,这里我们先看一下插入源码吧:
插入操作的入口方法是put(K,V),但核心逻辑在V putVal(int, K, V, boolean, boolean)方法中。putVal 方法总结起来就是如下过程:
1.当桶数组 table 为空时,通过扩容的方式初始化 table
2.查找要插入的键值对是否已经存在,存在的话根据条件判断是否用新值替换旧值
3.如果不存在,则将键值对链入链表中,并根据链表长度决定是否将链表转为红黑树
4.判断键值对数量是否大于阈值,大于的话则进行扩容操作