1.底层结构:
jdk1.7:数组+链表
jdk1.8:数组+链表+红黑树
数组的查询速度快,链表的修改速度快
2.默认值
默认初始容量为16(1<<4),必须是2的幂
如果隐式指定了更高的值,则使用的最大容量,但必须是2的幂并且最大不大于2的30次方
默认负载因子为0.75,负载因子=数据实际大小 / 分配的内存空间。例如数据占用空间为70,分配的内存空间为100,负载因子就是0.7
生成红黑树的阈值
红黑树转链表阈值,为了防止频繁的在临界值增加和删除所以2个阈值不一样
3.如何存储和获取值
jdk1.7
put
我们知道,如果我们想往数组中存入数据,我们首先得有一个数组下标,而我们在进行PUT的时候并不需要再传一个参数来作为数组的下标,那么HASHMAP中下标是怎么获取来的呢?答案为哈希算法,这也是为什么叫HashMap而不叫其他Map。
对于哈希算法,了解过的人应该都知道它需要一个哈希函数,这个函数接收一个参数返回一个HashCode,哈希函数的特点是对于相同的参数那么返回的HashCode肯定是相同的,对于不相同的参数,函数会尽可能的去返回不相同的HashCode,所以换一个角度理解,对于哈希函数,给不相同的参数可能会返回相同的HashCode,这个就叫哈希冲突或哈希碰撞。
但是通过直接hash算法得到的是很大的一个数组,显然不适合作为数组下标
于是解决方案:先将key进行hashCode,然后进行位运算
这个方法中h代表hashcode,length代表数组长度。我们发现它是用的逻辑与操作
对于数组长度为16的集合
h: 0101 0101
15: 0000 1111
&
0000 0101
15的高四位都是0,低四位都是1,而与操作的逻辑是两个运算位都为1结果才为1,所以对于上面这个运算结果的高四位肯定都是0,而低四位和h的低四位是一样的,所以结果的取值范围就是h的低四位的一个取值范围:0000-1111,也就是0至15,所以这个结果是符合数组下标的取值范围的。
这样就可以得到数组下标,但是这也会产生一个新问题,就是下标可能会相同,这就是哈希冲突或哈希碰撞,在HashMap中采用链表解决了这个问题,详解请看下一个标题
get
或取值时将key进行hash,然后再运算出下标,通过下标找到链表,然后再遍历链表判断key是否和查询的相同
jdk1.8
put
获取下标的方式同1.7一样
/*K代表键的数据类型,V代表值是数据类型,调用put方法需要传key和value
*调用putVal方法,将key进行一次hash传入第一个参数中
*/
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i; //声明变量
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; //声明局部变量
//p.hash是将p节点的key进行hash,并且判断p节点的key是否和要插入的key相同(保证map中的key不能重复)
if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) //hash值相同,但不一定是同一个key
e = p; //如果有这个key,就p节点赋值给e,为了下面返回oldValue
else if (p instanceof TreeNode) //判断p是不是树节点(红黑树)
e = ((HashMap.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;
}
}
//此处判断插入数据时是否遇到了同一个key,没有就返回null,有就返回旧节点的value
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
//扩容,判断是否大于阈值
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
树化,并不是链表长度达到8就树化,具体原因看下面代码
final void treeifyBin(HashMap.Node<K,V>[] tab, int hash) {
int n, index; HashMap.Node<K,V> e;
//判断数组长度是否小于最小表容量,如果是则不会树化,会通过扩容来增加散列,减小链表长度
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();
else if ((e = tab[index = (n - 1) & hash]) != null) {
HashMap.TreeNode<K,V> hd = null, tl = null;
do {
HashMap.TreeNode<K,V> p = replacementTreeNode(e, null);
if (tl == null)
hd = p;
else {
p.prev = tl;
tl.next = p;
}
tl = p;
} while ((e = e.next) != null);
if ((tab[index] = hd) != null)
hd.treeify(tab);
}
}
这个值是可以修改的
4.哈希碰撞
不同的hashCode一定对应不同的对象,
但不同的对象的hashCode可能相同
在上一节中产生的哈希冲突在jdk1.7和jdk1.8中的解决方案稍有不同,1.7采用头插法,1.8采用的时尾插法,这个不同也是因为1.8采用了红黑树
首先来看jdk1.7中,如下图,当Entry4需要插入到Entry1这个位置时,只有2种方案,插入头部或尾部,但是插入到尾部需要遍历影响效率,所以采用头插法。但是该链表是单向链表,插入到头部的数据不能被访问到,所以只将数据插入到头部是行不通的,还需要将链表整体向下位移一位,这样就可以访问链表中的所有数据了
在jdk1.8中,HashMap引入了红黑树,在插入数据时需要判断链表是否达到长度8,所以需要遍历链表,所以当遍历到最后一个节点时顺便把数据插入进去,这样就不需要像头插法那样移动链表了
5.扩容机制
1.7中:先扩容,再添加元素
判断集合大小是否大于阈值(阈值=负载因子*容量)并且要插入的元素所在节点是否有元素,有就扩容,为了防止链表太长导致查询效率低
扩容会生成一个新的数组,然后将旧数组上的数据转移到新数组上。当然这并不是简单的按下标赋值,需要重新计算下标,因为数组的长度不一样了,下图这个方法计算的下标旧可能发生变化。因为与运算时(length-1)变化了,这样可以使散列表分布更加分散。
1.8中:先添加元素再扩容
详情请看添加元素的源码注释
转载:https://blog.csdn.net/gtaaaa/article/details/105934300