这里聊JDK1.8的源码,采用图文结合的方式,文章会有些长,1.8的可读性实在没有1.7的好,请大家跟着我的思路,耐心的去看.简单的new 一个HashMap ,put一个k,v 进去.
一.先看1
无参的构造方法没什么可说的,看下注释Constructs an empty HashMap with the default initial capacity (16) and the default load factor (0.75).
, 直接给一个默认16 负载因子为0.75的HashMap对象.
有参的this
点进去看
上面都是一些判断抛异常的,主要是红框内的方法
这段是什么意思呢?看下注释 Returns a power of two size for the given target capacity.(返回一个大于等于cap的2的幂次函数)
也就是说 有参构造方法传10 Map的容量是 16 ,传 17 容量就是32.
new 一个HashMap 实际上并没有初始化,在执行put方法时,才会真正的初始化,1.8的初始化和扩容都是在同一个resize()方法里的,下面的源代码我加了注释
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table; // 初始化时 此时 oldTab 为null 会按顺序执行 步骤一 二 三 四
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; // double threshold
}
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
else { // zero initial threshold signifies using defaults
//步骤一
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"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];//步骤二
table = newTab;//步骤三
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 { // preserve order
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;//步骤四
}
初始化完成后,HashMap是怎样存储k,v的呢? 注意这段代码
这里 ,n是 HashMap 内部Node<K,V> 数组的长度,他是通过数组长度减1 与hash 的一个运算计算出key在Node数组中的下标的,这里引申出了一个面试题: HashMap的容量为什么是2的幂次函数?,我们先来思考一下,HashMap对存储数据的要求是能够随机并且均匀 的分散在Node数组中 ,不能 在某个位置聚合,比如index[8]的位置已经形成了很长的链表,但其他位置还没有数据.理解了这个 ,再来看下面的例子
以初始化容量16 举例 16 的二进制数为 0001 0000,如果与随机生成的hashcode 例如 0011 1111进行&运算,同为1 才为1 否则为0.因为16 的高三位和低四位都是0 所以无论hashcode怎么变化,最终的结果都只有两个 要么 高第四位为1 要么为0 很明显不能满足容量为 16 的Node数组的索引0~15的需求.n-1=15 15 的二进制 0000 1111,那么 和hashcode的&运算产生的低四位的结果 是在 0000-1111区间内,恰好满足0~15的需求,后期扩容也是如此.总结: HashMap的容量为2的幂次函数,归根结底是为了和hashcode进行&运算,使值可以分散的分布在Node数组中的每个索引,增加空间利用率,不至于使某个索引位的链表长度过长
看下put方法做了哪些事?
1.计算出key所在索引位置,如果为null,直接生成一个新的Node对象并赋值
2.如果不为空判断新的key与旧的key 是否相同,如果相同,则进行值覆盖
3.如果两个key不相同,则判断当前key所在的索引位置的下一个节点(next)是否为空 如果为空,则生成一个node赋值给当前key所指向的下一节点(尾插法).
这里又引申出一个问题,大家都知道HashMap是非线程安全
的,在put方法里就可以说明这点,试想A,B两个线程通知执行put方法,A走到了操作1 的位置判断当前索引位置为空,但是这时A线程挂起了,B线程这时对该索引位置赋值,然后A线程恢复执行,再赋值,是不是就把B的值给覆盖掉了?另外看下图,多线程走到这里,如果两个线程同时走到这里,也会导致重复的resize().
解决这一问题的办法Java中提供了 HashTable,ConcurrentHashMap 等办法,HashTable因为性能受限,基本已经淘汰了,下一篇从源码上分析ConcurrentHashMap 为什么是线程安全的.
感谢耐心收看,欢迎批评指导