HashMap集合原理
0、问题
- hashmap的结构是什么?(数组 + 链表 + 红黑树)
- 为什么要进行扩容?何时扩容?每次扩容多少?(均匀散列,提高效率。默认每次扩容为原来的一倍)
- key值是如何定位在table数组中的索引位置的?(key的 hashcode & (table数组长度 - 1))
- 如何得到的散列值?如何尽可能确保key均匀且高效的散列到数组中? hash & (table数组长度 - 1)
- 为什么不用hashcode取模运算取得hash值?而是使用hashcode & (length - 1)? 取模运算开销大
- 为什么hashMap集合的table容量值建议设置为2的n次方?(散列均匀、碰撞几率小,与运算有关系)
- 自定义对象作为key时要注意什么?(覆写hashcode和equals)
- map的put( key , value ) 和get( key )原理?
1、hashmap的结构
hashmap结构由数组和链表组成(jdk8以后是数组+链表+红黑树)。
在jdk8以上,每当链表长度大于8时自动将链表结构转换为红黑树(提高搜索效率)。
2、扩容机制
扩容是一个非常耗时的过程,因为它需要重新计算这些数据在新table数组中的位置并进行复制处理。所以如果我们已经预知HashMap中元素的个数,那么预设元素的个数能够有效的提高HashMap的性能 。阿里开发手册也建议:在new HashMap<>(size)的时候尽量指定初始大小。
1、三个构造函数:
HashMap():构造一个具有默认初始容量 (16) 和默认加载因子 (0.75) 的空 HashMap。
HashMap(int initialCapacity):构造一个带指定初始容量和默认加载因子 (0.75) 的空 HashMap。
HashMap(int initialCapacity, float loadFactor):构造一个带指定初始容量和加载因子的空 HashMap。
2、为什么要扩容?
当map集合中数据越来越多的时候, 碰撞的几率也就越来越高 ,链表长度也会变得很长,每次插入或获取一个数据需要遍历链表,当链表非常长的时候遍历链表是非常耗时的,所以需要扩充容量来提高效率。
3、何时扩容?扩容为多大?
默认是当table表中空间使用程度达到 HashMap() 容量 * 加载因子(默认0.75) 的时候自动进行扩容。
每次扩容大小为原来的一倍。
4、如何扩容?
当table表中空间使用程度达到 HashMap() 容量 * 加载因子(默认0.75) 的时候进行自动扩容,由于数组没有办法直接增大容量,所以首先要新建一个数组(容量为原来容量的2倍),然后然后重新计算每个元素在新数组中的位置(这个操作非常耗时,但是由于底层一系列算法实现,将效率已经变得很高了,扩容后一条数据要么在原位置、要么是在原位置再移动2次幂的位置 )
5、扩容源码:
void resize(int newCapacity) { //传入新的容量
Entry[] oldTable = table; //引用扩容前的Entry数组
int oldCapacity = oldTable.length;
if (oldCapacity == MAXIMUM_CAPACITY) { //扩容前的数组大小如果已经达到最大(2^30)了
threshold = Integer.MAX_VALUE; //修改阈值为int的最大值(2^31-1),这样以后就不会扩容了
return;
}
Entry[] newTable = new Entry[newCapacity]; //初始化一个新的Entry数组
transfer(newTable); //!!将数据转移到新的Entry数组里
table = newTable; //HashMap的table属性引用新的Entry数组
threshold = (int) (newCapacity * loadFactor);//修改阈值
}
数组元素拷贝源码:
void transfer(Entry[] newTable) {
Entry[] src = table; //src引用了旧的Entry数组
int newCapacity = newTable.length;
for (int j = 0; j < src.length; j++) { //遍历旧的Entry数组
Entry<K, V> e = src[j]; //取得旧Entry数组的每个元素
if (e != null) {
src[j] = null;//释放旧Entry数组的对象引用(for循环后,旧的Entry数组不再引用任何对象)
do {
Entry<K, V> next = e.next;
int i = indexFor(e.hash, newCapacity); //!!重新计算每个元素在数组中的位置
e.next = newTable[i]; //标记[1]
newTable[i] = e; //将元素放在数组上
e = next; //访问下一个Entry链上的元素
} while (e != null);
}
}
}
3、数据插入和取得流程
a)、插入数据:
-
判断是否为空,为空则调用putForNullKey方法,保存null与table第一个位置中,这是HashMap允许为null的原因
-
当向hashmap中插入一条数据Node时(key-value),先获取key的hashcode,然后通过运算hashCode & (table数组长度 - 1)得到一个hash值,通过该hash值去table数组中找对应的位置。
-
检查该位置上是否已经有元素,若没有,则直接将Node放在该位置。
-
若该位置上已有元素,则遍历链表,使用key的hash值与equals()方法逐一去与链表的节点比较。
a)、若有key值相同的节点,则直接替换value值为新值。
b)、若没有key值相同的节点,则直接将数据插在链表头部。
** 注意 :**在jdk8以后,插入链表前首先要对链表长度进行检查,检查是否大于8,若大于8则自动转换为红黑树结构
public V put(K key, V value) {
//当key为null,调用putForNullKey方法,保存null与table第一个位置中,这是HashMap允许为null的原因
if (key == null)
return putForNullKey(value);
//计算key的hash值
int hash = hash(key.hashCode()); ------(1)
//计算key hash 值在 table 数组中的位置
int i = indexFor(hash, table.length); ------(2)
//从i出开始迭代 e,找到 key 保存的位置
for (Entry<K, V> e = table[i]; e != null; e = e.next) {
Object k;
//判断该条链上是否有hash值相同的(key相同)
//若存在相同,则直接覆盖value,返回旧value
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value; //旧值 = 新值
e.value = value;
e.recordAccess(this);
return oldValue; //返回旧值
}
}
//修改次数增加1
modCount++;
//将key、value添加至i位置处
addEntry(hash, key, value, i);
return null;
}
b)、取得:
- hashmap.get(“key”)方法在map集合中通过key获取数据,首先通过hash运算取得key的hashcode,
- 然后将hashcode & (length - 1)得到hash散列值,通过该值定位到table数组中的具体索引位置。
- 遍历链表,通过hashcode和equals比较key是否相同。若相同,则返回对应的value值,否则返回null
public V get(Object key) {
// 若为null,调用getForNullKey方法返回相对应的value
if (key == null)
return getForNullKey();
// 根据该 key 的 hashCode 值计算它的 hash 码
int hash = hash(key.hashCode());
// 取出 table 数组中指定索引处的值
for (Entry<K, V> e = table[indexFor(hash, table.length)]; e != null; e = e.next) {
Object k;
//若搜索的key与查找的key相同,则返回相对应的value
if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
return e.value;
}
return null;
}
4、其他
1、如何得到key在table数组中该存放在哪个位置?
通过 ** h & (table数组容量 - 1) **运算的到key在table数组中的索引位置!
2、为什么hashMap集合的table容量值建议设置为2的整数次幂次方 ?
首先 hashmap的数组容量大小都是2的次方大小时,hashmap的效率最高 。
因为当向map中插入或获取一个元素的时候,key在table数组中的索引位置,使用的是 h & (table数组容量 - 1) 算法来计算的,h可以是根据key计算出来的任何值,既然h可能是任意值,那么只有在数组容量上做文章,为了使数据尽可能均匀的分布在table数组上,那么数组的容量大小则至关重要。下面引用一个例子来说明:
看下图,左边两组是数组长度为16(2的4次方),右边两组是数组长度为15。两组的hashcode均为8和9,但是很明显,当它们和1110“与”的时候,产生了相同的结果,也就是说它们会定位到数组中的同一个位置上去,这就产生了碰撞,8和9会被放到同一个链表上,那么查询的时候就需要遍历这个链表,得到8或者9,这样就降低了查询的效率。同时,我们也可以发现,当数组长度为15的时候,hashcode的值会与14(1110)进行“与”,那么最后一位永远是0,而0001,0011,0101,1001,1011,0111,1101这几个位置永远都不能存放元素了,空间浪费相当大,更糟的是这种情况中,数组可以使用的位置比数组长度小了很多,这意味着进一步增加了碰撞的几率,减慢了查询的效率!