参考文章:
Java 8系列之重新认识HashMap
HashMap根据key的hashcode值存储数据,大多数情况下可以直接定位到值,因而具有很快的访问速度,但遍历顺讯确实不确定的。HashMap最多允许一条记录的key为null,允许多条记录的value为null。同时HashMap是非线程安全的,即任一时刻可以有多个线程同时写HashMap,可能会导致数据不一致。
从结构上讲,HashMap是数组+链表的结合体,每一个键值对都被包装为Entry类。当执行put(key,value)的方法时,通过计算当前key的哈希值来决定该键值对在table数组中的下标i。如果table[ i ]为空,则直接插入;如果不为空,循环遍历table[ i ]的链表,如果有键值对的key与当前key相同,直接覆盖该键值对;如果没有相等的键值对,将当前键值对添加至链表尾部。
1、put实现
public V put(K key, V value) {
/* 如果key值为null,调用putForNullKey,不同版本该方法的实现也不同,
1.该方法将从头遍历table数组,将key为null的键值对放在数组中第一个为空的地方。
2.用中间变量标记null 键值对。*/
if (key == null)
return putForNullKey(value);
//key不为null时,计算当前key的hash值。
int hash = hash(key.hashCode());
//计算该hash值对应的数组下标i
int i = indexFor(hash, table.length);
/* 遍历table[i]中的Entry链表,如果不存在hash值相同的键值对,直接插入数组,
如果存在hash值相同的键值对,比较两个键值对的key,如果key相同,用当前键值对覆盖此键值对,
如果key不同,直接插入。
*/
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
addEntry(hash, key, value, i);
return null;
}
2、hash()、indexFor()实现——确定键值对在table中的索引
我们希望HashMap中的元素位置尽量分布均匀,尽量使得每个位置上的元素数量只有一个,那么当用hash算法求得这个位置时,马上就可以知道对应位置的元素就是目标元素,不用遍历链表,大大优化了查询效率。HashMap定位数组索引位置,直接决定了hash方法的离散性能。
static final int hash(Object key) { //jdk1.8 & jdk1.7
int h;
// h = key.hashCode() 为第一步 取hashCode值
// h ^ (h >>> 16) 为第二步 高位参与运算 为了让高低位都充分参与运算
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
static int indexFor(int h, int length) { //jdk1.7的源码,jdk1.8没有这个方法,但是实现原理一样的
return h & (length - 1); //第三步 取模运算 h按位与上length - 1等价于h对length取模,使得元素分布均匀
}
3、resize()实现——table的扩容机制
table的默认初始长度为16,当无法装在更多元素时,就需要扩大数组长度调用resize方法。
由于在进行扩容时,是用更大容量的数组代替之前的小容量数组,并且会对原来的键值对重新计算排序,特别耗性能,所以在创建HashMap的时候最好把大概容量写上,避免进行扩容操作。另外,之所以hashmap不支持多线程操作,是因为resize扩容是在把小容量table的数据转移至大容量数组时,会让数组错位造成调用transfer时造成死循环。
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);//修改阈值
}