面试中HashMap几乎是必问的,问到hashmap必然离不开扩容机制,话不多说,肝就完了。
我们知道HashMap底层是采用数组+链表来实现的,jdk1.8后又引入了红黑树,本文针对JDK1.8后的HashMap进行分析。
先说明一下HashMap中几个重要的字段:
- table:数组
- size:元素的个数,即hashMap中存放了多少个元素,有别于capacity(容量)(容量是指数组的长度!)
- threshold:扩容阈值,等于负载因子*容量。
还有几个默认的常量:
1.默认负载因子0.75
2.默认容量16
在构造HashMap时没有传入参数就会使用默认的值。
先介绍一下三个构造方法,需要注意的是:在调用构造方法时不会去执行resize(扩容)操作,在首次put元素时才会resize。
1.构造方法
无参构造:负载因子默认为0.75
源码如下:
/**
* Constructs an empty {@code HashMap} with the default initial capacity
* (16) and the default load factor (0.75).
*/
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
一参构造:指定初始容量大小,调用了另外一个两参构造方法,参数分别是容量大小和默认的负载因子0.75。
源码如下:
/**
* Constructs an empty {@code HashMap} with the specified initial
* capacity and the default load factor (0.75).
*
* @param initialCapacity the initial capacity.
* @throws IllegalArgumentException if the initial capacity is negative.
*/
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
两参构造方法。
源码及分析如下:
/**
* Constructs an empty {@code HashMap} with the specified initial
* capacity and load factor.
*
* @param initialCapacity the initial capacity
* @param loadFactor the load factor
* @throws IllegalArgumentException if the initial capacity is negative
* or the load factor is nonpositive
*/
public HashMap(int initialCapacity, float loadFactor) {
//容量大小小于零抛出异常
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
//容量大小大于最大容量,将指定为最大容量1<<30,注意:容量大小永远是2的整数幂
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
//如果负载因子不是数值或者小于零,抛出异常
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
//负载因子为指定的负载因子
this.loadFactor = loadFactor;
//threshold为下一次触发扩容的阈值
this.threshold = tableSizeFor(initialCapacity);
}
其中tableSizeFor方法源码如下:
/**
* Returns a power of two size for th given target capacity.
*/
static final int tableSizeFor(int cap) {
int n = -1 >>> Integer.numberOfLeadingZeros(cap - 1);
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
这个方法是用来返回一个大于输入参数且最近的2的整数次幂的数,例如参数是10,则返回16。知道这个作用就行了。
2.Put方法
put方法调用了putVal方法,源码如下:
/**
* Associates the specified value with the specified key in this map.
* If the map previously contained a mapping for the key, the old
* value is replaced.
*
* @param key key with which the specified value is to be associated
* @param value value to be associated with the specified key
* @return the previous value associated with {@code key}, or
* {@code null} if there was no mapping for {@code key}.
* (A {@code null} return can also indicate that the map
* previously associated {@code null} with {@code key}.)
*/
public V put(K key, V value) {
//调用了putVal()方法,hash(key)是计算key的hash值,具体为key的hashcode(简称为h)与h无符号右移16位进行异或运算。
//注:一个良好的hash计算方法应尽可能减少hash冲突,此hash方法通过无符号右移16使高16位也能参与运算,用来减少hash冲突
return putVal(hash(key), key, value, false, true);
}
putVal()方法流程:
- 首先判断table是否为null,如果为null就进行resize。(首次put元素会执行resize);
- 如果table不为null,计算元素要存放位置的下标,如果该下标处没有元素,则将元素直接放到此位置。
- 否则说明此位置已经有元素了。分三种情况:
①判断此元素key值与要存放的key是否是同一对象或者相等,如果是则直接覆盖此元素的值并返回被覆盖的值。
②判断此元素是否是红黑树节点,如果是,调用红黑树的putTreeVal()方法存放元素。
③否则就是一个链表,对链表进行遍历。遍历过程中,比较每个节点的key是否与要放入元素的key值是否是同一对象或者相等,如果遇到相等,则跳出遍历过程并返回被覆盖的值;如果遍历完都没有发现key相等,则将元素放到链表末尾,然后判断是否需要进行树化操作。 - 更新size(上面的操作中,可能是新增元素,也可能是覆盖已有的元素,如果新增就++size,如果是覆盖不会执行第4步),如果size>threshold则进行resize。
源码及分析如下:
3.resize方法
resize流程:
- 如果是非第一次扩容(初始化扩容),则进行两倍扩容,然后将原来的元素重新散列到新数组中,并返回新数组。
- 如果是第一次扩容,如果没有参数,按照默认的容量大小16和负载因子0.75进行初始化,并返回新数组。如果指定了初始容量或负载因子就按照指定的数值初始化,并返回新数组。
源码及分析如下:
再分析一下散列方法:
- 在jdk1.7时,是通过对每一个元素重新计算hash值进行散列的。
- 而在1.8则是:通过元素的hash值与原数组长度进行与运算,结果为0则代表在新数组中的下标为
原下标
,结果为1代表在新数组中的下标为原下标+原数组长度
。