对HashMap的数据结构和工作原理进行学习。
一、数据结构
随着版本的不同,HashMap的数据结构不同。
版本 | 数据结构 |
---|
jdk 1.7及以前 | 数组+链表 | jdk 1.8后 | 数组+链表+红黑树 |
- 数组:
HashMap的主干数据结构是数组,即key经过hash计算后将节点对象存放到数组对应的位置上 - 链表:
当多个不同的key经过hash计算后得到的值相同,那么在数组对应位置上存储多个对象的链表,每个对象的属性除了key,value外还有next,即指向下一个节点的地址 - 红黑树
jdk1.8以后,如果某一条链表的长度超过8(默认),则转为红黑树的存储方式
二、工作原理
无参始化hashmap时,数组的初始容量 为16,负载因子为0.75。容器的阈值等于 初始容量 × 负载因子,默认情况下阈值为 16 * 0.75 = 12;
2.1 put过程
第一步: hashMap使用自己的hash算法,算出传入的key的hash值;
第二步: 用key的hash值调用indexOf()找到对象在数组中应该存放的位置;
第三步: 当size(容器中已有Entry的数量)大于阈值时,进行扩容,将数组长度扩容为原来的2倍
第四步: 将hash、key、value封装成一个Entry,存放到数组计算出来的位置中
- 位置无元素,放在该位置;
- 存在链表,则进行遍历,通过key进行equals比较:
true:返回原来的value,将当前的value替换掉原来的 false:将Entry对象的next指向改位置的初始节点(成为链表的第一个位置)
2.2 get过程
get过程的原理就比较简单,根据key的hashCode算出数组中的下标,遍历Entry链表,直到找到元素,源码如下:
public V get(Object key) {
if (key == null)
return getForNullKey();
Entry<K,V> entry = getEntry(key);
return null == entry ? null : entry.getValue();
}
private V getForNullKey() {
if (size == 0) {
return null;
}
for (Entry<K,V> e = table[0]; e != null; e = e.next) {
if (e.key == null)
return e.value;
}
return null;
}
final Entry<K,V> getEntry(Object key) {
if (size == 0) {
return null;
}
int hash = (key == null) ? 0 : hash(key);
for (Entry<K,V> e = table[indexFor(hash, table.length)];
e != null;
e = e.next) {
Object k;
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
}
return null;
}
- get的时候,如果key==null,判断Map的长度也为空的话就返回null,如果Map长度不为空,并且存在Entry,遍历table[0],返回e.value.
- getEntry的时候,首先要获取key的hash值,通过hash和table.length获取到的hashCode值得到entry在数组中存放的位置,如果传入的key与遍历的元素的key的hash相等的话并且key.equals为true,则返回entry;(如果返回的entry不为空的话,则返回entry的value值)。
2.3 负载因子
2.3.1 作用
用来计算阈值(初始容量 × 负载因子),当容量打到阈值时自动触发扩容机制。
2.3.2 为什么是0.75
这是时间和空间的权衡,是一个折中的选择。
- 负载因子太高,虽然提高了空间利用率,但是会导致大量的hash冲突,降低查询效率。
- 当负载因子为0.5或者更低的时候,hash冲突降低从而提高了查询效率,但原本需要1M的存储空间,现在需要2M甚至更大的存储空间,结果就是空间利用率降低了。
|