HashMap
HashMap在JDK1.8里发生了一点变化,我们先来说JDK1.6,搞懂1.6之后更容易理解1.8 。
HashMap的初始化
HashMap有三个构造函数:
- HashMap():构建一个初始容量为 16,负载因子为 0.75 的 HashMap。
- HashMap(int initialCapacity):构建一个初始容量为 initialCapacity(注),负载因子为 0.75 的 HashMap。
- HashMap(int initialCapacity, float loadFactor):以指定初始容量(注)、指定的负载因子创建一个 HashMap。
注:HashMap初始化时,容量并不是initialCapacity,而是大于initicalCapacity的最小的2的n次方的数,如 new HashMap(10),容量会被设成16 。
构造函数主要做的事有:
-
判断 initicalCapacity,小于0抛
IllegalArgumentException
,大于最大容量时设为最大容量; -
判断 loadFactor(负载因子),小于等于0抛异常;
-
计算initicalCapacity,也就是(注)里说的;
-
设置负载因子;
-
初始化数组。
源码为:
// 以指定初始化容量、负载因子创建 HashMap
public HashMap(int initialCapacity, float loadFactor)
{
// 初始容量不能为负数
if (initialCapacity < 0)
throw new IllegalArgumentException(
"Illegal initial capacity: " +
initialCapacity);
// 如果初始容量大于最大容量,让出示容量
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
// 负载因子必须大于 0 的数值
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException(
loadFactor);
// 计算出大于 initialCapacity 的最小的 2 的 n 次方值。
int capacity = 1;
while (capacity < initialCapacity)
capacity <<= 1;
this.loadFactor = loadFactor;
// 设置容量极限等于容量 * 负载因子
threshold = (int)(capacity * loadFactor);
// 初始化 table 数组
table = new Entry[capacity];
init();
}
HashMap的数据结构
1.6中,HashMap采用 位桶+链表 的方式,也就是散列链表,来存储键值对对象Entry。
散列链表table是一个Entry数组,Entry可以看作是一个头插单链表,链表头存放在table中。
插入时,就是通过hash值计算元素要放在table数组的哪个位置,然后在这个位置对应的Entry链表上进行插入操作。
put操作
插入过程,大致过程为:
-
空键直接放到空键对应的值上;
-
计算hash值并获取hash值在table表中的索引;
-
在索引对应的Entry链表上查找键
-
覆盖Entry对象的值(找到相同键)或新建Entry对象(没有相同键)
具体流程如下:
原理知道了,再来看对应的代码,流程与上面是对应的
右边是一些实现细节,一个是hash值的计算,一个是获取hash值在table中的索引,一个是添加Ehtry对象的操作。注意,这里的hash值不是对象的hashcode,看流程图的第三步,是 hash(key.hashCode)
。hash()这个函数的计算是个纯粹的数学计算,就不多说了,来看indexFor():
return h & (length - 1);
有人可能要问了,就这么一句,就能找到对应的索引位置?
这个地方巧妙在,它与HashMap的初始化和扩充联系了起来:
初始化时,table的初始大小一定是2的n次方;
扩充时,在右边第三个,addEntry方法内的最后,可以看到hashmap到达一定容量会扩充,且每次都是原来的二倍,这样,table的大小一定是2的n次方,对应的二进制值一定是 100...00
,上面的 (length - 1)
一定是 11...11
,那么其他数与 11...11
的 &
操作一定是 这个数 % 11...11
,我们假设length = 16,length - 1 = 15,那么 int & (length - 1)
时:
101 & 1111 = 101 // 5 & 15 = 5
1111 & 1111 = 1111 // 15 & 15 = 15
10000 & 1111 = 0 // 16 & 15 = 0
10001 & 1111 = 1 // 17 & 15 = 1
这样就保证了计算出的索引值总是小于length。
再看addEntry(),有人可能又要问了,为什么是头插不是尾插?
我们假设hash对应的索引处还没有Entry对象放进来,table[bucketindex] == null,如果这时进行尾插,那么会报空指针异常,再判断是否为null又会增加操作。
如果头插,新对象指向旧对象,那么不管旧对象是不是null都可以插入成功,也无需加判空。
这里是put的源代码:
public V put(K key, V value) {
// 如果 key 为 null,调用 putForNullKey 方法进行处理
if (key == null)
return putForNullKey(value);
// 根据 key 的 keyCode 计算 Hash 值
int hash =