详解HashMap
一、数据结构
HashMap是由Hash表(散列表)维护的一个数据结构模型,什么是Hash表呢?
哈希表,是根据Key-value直接进行访问的数据结构,也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表:记录的存储位置=f(关键字)。
首先我们来看看HashMap源码中的“静态类”Entry:
static class Entry<K,V> implements Map.Entry<K,V> {
final K key;
V value;
Entry<K,V> next;
}
源码过多,我们只展示其数据结构,是一个典型的链式数据结构,完全可以推测出HashMap解决Hash冲突的方式可能为链地址法。
二、HashMap中的主要方法
1.put()方法
依旧根据源码进行分析,对其数据结构进行更深入的验证和分析:
public V put(K key, V value) {
if (table == EMPTY_TABLE) {
inflateTable(threshold);
//当table为空时,传入一个临界值,构造一个新的table,table为一个数组,存放多个Entry,该方法只允许最终数组大小为2的幂
}
if (key == null)
return putForNullKey(value);
//允许加入一个key为空的value
int hash = hash(key);
//获取key的hash值,通过hash函数对key的HashCode进行处理得到的值
int i = indexFor(hash, table.length);
//根据Key的HashCode找到其在数组中的index:return h & (length-1);也就是使用数组长度求余。
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
//循环遍历数组中指定index中的Entry链表
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {//当key的HashCode值相同,且两者equals为true,开始覆盖原来的key
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
//否则表示key为空,插入新值
modCount++;
addEntry(hash, key, value, i);//加入一个新的Entry
return null;
}
2.get()方法:
源码:public V get(Object key) {
if (key == null)
return getForNullKey();
Entry<K,V> entry = getEntry(key);
return null == entry ? null : entry.getValue();
}
get方法相对简单,如果key为null则调用getForNullKey方法,否则getEntry,获取指定entry。
3.总结
由以上源码我们可以得出,HashMap解决冲突的方式是链地址法。Hash函数,为key的HashCode值,与Hash表表长求余,余数为插入表的index,若有冲突,则在该表的Entry后面链式插入。同时,可以根据源码细节,看出HashMap允许key为null。
三、addEntry()细节和HashMap的扩容
addEntry()源码: //在table指定位置新增Entry, 这个方法很重要
void addEntry(int hash, K key, V value, int bucketIndex) {
if ((size >= threshold) && (null != table[bucketIndex])) {
//table容量不够, 该扩容了(两倍table),重点来了,下面将会详细分析
resize(2 * table.length);
//计算hash, null为0
hash = (null != key) ? hash(key) : 0;
//找出指定hash在table中的位置
bucketIndex = indexFor(hash, table.length);
}
createEntry(hash, key, value, bucketIndex);
}
//扩容方法 (newCapacity * loadFactor)
void resize(int newCapacity) {
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
//如果之前的HashMap已经扩充打最大了,那么就将临界值threshold设置为最大的int值
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
}
//根据新传入的capacity创建新Entry数组,将table引用指向这个新创建的数组,此时即完成扩容
Entry[] newTable = new Entry[newCapacity];
transfer(newTable, initHashSeedAsNeeded(newCapacity));
table = newTable;
//扩容公式在这儿(newCapacity * loadFactor)
//通过这个公式也可看出,loadFactor设置得越小,遇到hash冲突的几率就越小
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}
//扩容之后,重新计算hash,然后再重新根据hash分配位置,
//由此可见,为了保证效率,如果能指定合适的HashMap的容量,会更合适
void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
for (Entry<K,V> e : table) {
while(null != e) {
Entry<K,V> next = e.next;
if (rehash) {
e.hash = null == e.key ? 0 : hash(e.key);
}
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;
}
}
}
在addEntry时,会判断是否到了当前HashMap的容量临界值,如果到了,则进行扩容:
扩容方式是直接将HashMap的数组长度翻倍,默认数组的长度为16,负载因子为0.75f,临界值为负载因子乘以数组长度。扩容时机为key-value键值对也就是Entry的数量大于临界值时,进行扩容,并在扩容后将原来的元素重写排版。