简介
HashMap是根据键的hashCode值来存储数据,HashMap允许key、value为NULL,非线程安全。
数据结构
HashMap是数组和链表的结合体,数组中每一个元素是Entry,Entry是包含键值对的链表,Entry源码如下:
static class Entry<K,V> implements Map.Entry<K,V> {
final K key;
V value;
Entry<K,V> next;
final int hash;
……
}
存储
根据key的hashCode找到bucket位置来存储Entry对象,如果hashCode值相同发生“碰撞”,则将Entry对象存储在链表当中,源码如下:
public V put(K key, V value) {
// HashMap允许存放null键和null值。
// 当key为null时,调用putForNullKey方法,将value放置在数组第一个位置。
if (key == null)
return putForNullKey(value);
// 根据key的keyCode重新计算hash值。
int hash = hash(key.hashCode());
// 搜索指定hash值在对应table中的索引。
int i = indexFor(hash, table.length);
// 如果 i 索引处的 Entry 不为 null,通过循环不断遍历 e 元素的下一个元素。
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;
}
}
// 如果i索引处的Entry为null,表明此处还没有Entry。
modCount++;
// 将key、value添加到i索引处。
addEntry(hash, key, value, i);
return null;
}
读取
根据key的hashCode找到bucket位置,然后循环遍历Entry链表,通过key的equals()方法找到对应的value,源码如下:
public V get(Object key) {
if (key == null)
return getForNullKey();
int hash = hash(key.hashCode());
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.equals(k)))
return e.value;
}
return null;
}
扩容
假设数组大小为n(默认值16),当元素个数大于n * loadFactor(默认值0.75),数组大小将扩容一倍,并将原来的对象放入新的bucket数组中,这个过程叫作rehashing,因为它将调用hash方法找到新的bucket位置,在调整大小的过程中,存储在链表中的元素次序会反过来,因为移动到新的bucket位置的时候,HashMap并不会将元素放在链表的尾部,而是放在头部,这是为了避免尾部遍历(tail traversing)。
在多线程环境下,可能产生条件竞争(race condition),因为如果两个线程都发现HashMap需要重新调整大小,它们会同时操作,所以是非线程安全。
总结
HashMap通过key的hashCode找到bucket位置,并将数据存放在Entry链表当中,或者通过key的equals方法循环遍历链表获取对应的value。实际使用时,应该提前预估HashMap的容量,并据此设置相应的数组大小,尽量避免扩容,因为它是一个非常耗时的过程。