JDK1.6,1.7中,HashMap采用数组+链表实现,用链表处理冲突,同一hash值的链表都存储在一个链表里。当位于同一链表中的元素较多时,通过Key值依次查找的效率较低。当有大量元素都存放到同一个数组元素时,这个数组元素下有一条长长的链表,假设链表有N个元素,遍历的时间复杂度为O(n),完全失去了它的优势。
JDK1.8中,HashMap采用数组+链表+红黑树(查找时间复杂度为O(logn))实现,当链表长度超过阈值8时,将链表转换为红黑树,这样可以减少查找时间。
HashMap的扩容
如果不指名初始化大小,默认大小为16,如果数组中的元素达到(填充比*Node.length)重新调整HashMap大小变为原来的2倍大小。
当链表数组的容量超过初始容量的0.75时,将链表数组扩大2倍,把原链表数组复制到新数组中。
问题:为什么需要填充比?为什么需要扩容?
如果填充比很大,说明利用的空间很多,如果不进行扩容的话,链表就会越来越长,这样查找的效率很低。
HashMap是以空间换时间,所以填充比没必要太大。填充比太小又会导致空间浪费。如果注重内存,填充比可以稍大,如果注重查找性能,填充比可以稍小。
HashMap实现原理
HashMap的主干是一个Entry数组,Entry是HashMap的基本单元,每一个Entry包含一个key-value键值对。
HashMap由数组+链表组成,数组是HashMap的主体,链表则是主要为了解决哈希冲突而存在。
HashMap的put操作
public V put(K key, V value) {
//如果table数组为空数组{},进行数组填充(为table分配实际内存空间),入参为threshold,此时threshold为initialCapacity 默认是1<<4(24=16)
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
//如果key为null,存储位置为table[0]或table[0]的冲突链上
if (key == null)
return putForNullKey(value);
int hash = hash(key);//对key的hashcode进一步计算,确保散列均匀
int i = indexFor(hash, table.length);//获取在table中的实际位置
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
//如果该对应数据已存在,执行覆盖操作。用新value替换旧value,并返回旧value
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++;//保证并发访问时,若HashMap内部结构发生变化,快速响应失败
addEntry(hash, key, value, i);//新增一个entry
return null;
}
HashMap的get操作
public V get(Object key) {
//如果key为null,则直接去table[0]处去检索即可。
if (key == null)
return getForNullKey();
Entry<K,V> entry = getEntry(key);
return null == entry ? null : entry.getValue();
}
final Entry<K,V> getEntry(Object key) {
if (size == 0) {
return null;
}
//通过key的hashcode值计算hash值
int hash = (key == null) ? 0 : hash(key);
//indexFor (hash&length-1) 获取最终数组索引,然后遍历链表,通过equals方法比对找出对应记录
for (Entry<K,V> e = table[indexFor(hash, table.length)];
e != null;
e = e.next) {
Object k;
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
}
return null;
}
get方法实现,key(hashcode)–>hash–>indexFor()–>最终索引位置,找到相应位置的table[i],再查看是否有链表,遍历链表,通过key的equals方法比对查找对应的记录。
重写equals方法需同时重写hashCode方法
我们在进行get和put时,使用的key从逻辑上讲是等值的(通过equals方法),但如果没有重写hashCode方法,所以put操作时,key(hashcode1)–>hash–>indexFor–>而通过key取出value的时候 key(hashcode1)–>hash–>indexFor–>最终索引位置,由于hashcode1不等于hashcode2,导致没有定位到一个数组位置而返回逻辑上错误的值null。