目录
HashMap是Map接口的一个实现类,hashMap中,将键和值封装成一个entry对象,其中键唯一,而值可以重复,二者都允许为null,此时存储在该数组下表为0 的位置。集合内部无序。
一、数据结构
在JDK1.8以前使用数组+链表存储
在 JDK1.8以后使用数组+链表+红黑树存储
如何存储
hashMap内部使用Node<K,V>类型的数组存储数据,在如下的源代码中,我们发现,Node<K,V>是Map.Entry<K,V>的实现类,因此我们所说的键和值被封装成entry对象实际上是被封装成了Node对象,该类中包含三个属性分别是key的hashcode,key,value以及next
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;//需要根据该hash值确认该对象在数组中位置
final K key;
V value;
Node<K,V> next;
Node(int hash, K key, V value, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
如何应对冲突(链表+红黑树)
当一个对象被put进map集合时,会执行put方法,在put方法中返回的putVal会调用hash(key)方法,该方法的目的是,获取键的hashcode并取其高16位,然后返回。
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
此时的hashcode被用来计算该对象在Node数组中的位置
1.在JDK1.8以前
用数组的长度与该hashcode取余,得出位置
2.在JDK1.8以后
由于取余的运算符%效率较低,因此在1.8后使用(n - 1) & hash 确定位置,此处的n为数组长度,n必须是2的n次幂。
if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null);
在计算后存入数组
//hashcode未冲突,new新的节点将键值对存入数组对应位置
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
但是在多次put后,hashcode会产生冲突的可能,一般来说会有两种冲突
冲突1:
该类型的hashcode相同的概率较高,为了降低该种概率,会将hashcode再次计算得到最终的hashcode
冲突2:
由于hashcode是一个int类型的数据,而int类型的最大值是2的32次方,因此不可能每个对象都有相同的hashcode
因此对于相同的hashcode,在向map中存入数据前,会通过比较二者的key是否一致。若一致,将旧的value更新。若不一致,则需要重新存储,但依然是在该位置上,不过是以链表的形式存储,该链表为单向链表,使用node<K,V>对象的next属性指向新的节点。
在经历多次put后,该数组的某个节点可能会产生一条规模较大的链表,此时会产生新的问题,即查找getKey(key)方法。从该方法的源代码中,我们可以观察到,在根据key查找时,会先计算该key的hashcode,找到对应的位置,然后从该位置开始遍历链表,查找指定元素。这个过程是非常繁复的。
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
因此当链表规模过时,为提了提高查询效率,链表会转换成一颗有序的树。当计算完hashcode后,与树种节点的hashcode进行比较,大于则向下比较左子树,小于比较右子树,以此提高查找效率。
二、扩容
在ArrayList数组扩容中,我们说到数组的扩容方式有三种,但对于HashMap来说并不适用。
第一次扩容
数组Node<K,V>数组被初始化后,长度为0,第一次put对象时,扩容发生,此时,数组table的长度为0,调用resize()方法,为数组开辟16个位置
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
后续扩容
在向数组中添加内容时,若hashcode重复,会在同一位置产生链表,当该链表的长度为8且数组长度小于64时,调用resize()方法咱找原数组大小的2倍进行扩容,扩容后,数组内的元素重新计算位置
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY) resize();
若大于64则将链表转换成树
加载因子(填充因子)
用于指定数组的实际使用率,当hashMap中的元素个数超过 数组大小*加载因子时,原数组进行扩容
1.使用无参构造初始化==>public HashMap()
数组默认长度为16,加载因子为0.75,即当数组元素个数大于12时,进行扩容
2.使用有参构造初始化==>public HashMap(int initCapacity,float loadFactor)
指定数组长度(必须是2的n次幂),指定加载因子