HashMap 是键值对存储,且键值唯一,链表与数组混合结构的一种数据结构,因其高速查询存储效率而使用效率高,以下是其源码解析
构造参数
// 默认容量
static final int DEFAULT_INITIAL_CAPACITY = 4;
// 最大容量
static final int MAXIMUM_CAPACITY = 1 << 30;
//默认载因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;
static final HashMapEntry<?,?>[] EMPTY_TABLE = {};
//存储数据的数据,需要时扩容,长度为2的倍数
transient HashMapEntry<K,V>[] table = (HashMapEntry<K,V>[]) EMPTY_TABLE;
//当前容量
transient int size;
//容量阈值,即存储一定的数据后,进行扩容
int threshold;
//使用的加载因子,默认为DEFAULT_LOAD_FACTOR
final float loadFactor = DEFAULT_LOAD_FACTOR;
//数据结构变动次数,如添加、删除时的变动
transient int modCount;
参数中要注意的是加载因子,如源码注释所说,加载因子其实是HashMap在时间和空间上的权衡,加载因子越大,空间利用率越大而查询等速率越低;反之加载因子越小,查询速率越高,占用空间也就越多。这与HashMap的结构有关。这里先看看HashMap的结构
数据在存储的时候,会通过hash函数获得在数组table上的地址,也就是说,有可能使得多个数据在table上的地址是相同的,当这种情况出现时,需要访问样的位置,会降低访问的效率。 因此,加载因子越低,或者hash函数越理想,就越能让数据均匀地分布在table上。 具体的访问方式在后面会有
构造
public HashMap(int initialCapacity, float loadFactor)
public HashMap(int initialCapacity)
public HashMap()
public HashMap(Map<? extends K, ? extends V> m)
四种构造方式,构造时会做容量检查,不做说明
访问操作
首先看插入数据put()
public V put(K key, V value) {
//检查table,为空将其初始化
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
//将key为null的键值对放到相应位置
if (key == null)
return putForNullKey(value);
//获取hash值
int hash = sun.misc.Hashing.singleWordWangJenkinsHash(key);
//通过hash值定位在table上的位置
int i = indexFor(hash, table.length);
for (HashMapEntry<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++;
// 插入新的HashMapEntry
addEntry(hash, key, value, i);
return null;
}
put()并不难理解,但是有需要看的地方,首先是inflateTable(),它会将table初始化,代码很简单,如下
private void inflateTable(int toSize) {
// 容量为2的位数
int capacity = roundUpToPowerOf2(toSize);
// 容量阈值
float thresholdFloat = capacity * loadFactor;
if (thresholdFloat > MAXIMUM_CAPACITY + 1) {
thresholdFloat = MAXIMUM_CAPACITY + 1;
}
threshold = (int) thresholdFloat;
table = new HashMapEntry[capacity];
}
再来是indexFor(), 通过table长度与hash值得与操作获得在table上的位置
static int indexFor(int h, int length) {
return h & (length-1);
}
然后,接下来的代码就没那么清晰了,因为关键的HashMapEntry还不知道是什么,那么有必要先看一下HashMapEntry
HashMapEntry
static class HashMapEntry<K,V> implements Map.Entry<K,V> {
final K key;
V value;
HashMapEntry<K,V> next;
int hash;
......
}
首先,HashMapEntry继承Map.Entry, 然后存有一个key和value, 并且指向下一个HashMapEntry的next, 这是典型的链式结构,也就是说put进来的键值对会封装成HashMapEntry,将其以链表的形式存储于数组table里。
返回去看put(), put()通过indexFor获得了数据(也就是键值对将会成为HashMapEntry),在table上的位置,然后便利该位置上的HashMapEntry链表,如果要插入的数据的hash值与当前访问到的HashMapEntry的hash值相等并且key值也相等,说明该key已经被使用,则此put()操作实际应该理解为更新,回去更新此key的value。如果遍历完链表,说明key没有被使用,理解为在当前table的位置插入一个新的HashMapEntry,代码会运行到addEntry()
HashMapEntry的添加
void addEntry(int hash, K key, V value, int bucketIndex) {
if ((size >= threshold) && (null != table[bucketIndex])) {
//扩容
resize(2 * table.length);
hash = (null != key) ? sun.misc.Hashing.singleWordWangJenkinsHash(key) : 0;
bucketIndex = indexFor(hash, table.length);
}
createEntry(hash, key, value, bucketIndex);
}
addEntry会先检查容量是否超过阈值,如果超过了就进行扩容,然后再通过createEntry()进行HashMapEntry的创建添加。
void resize(int newCapacity) {
HashMapEntry[] oldTable = table;
int oldCapacity = oldTable.length;
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
}
HashMapEntry[] newTable = new HashMapEntry[newCapacity];
transfer(newTable);
table = newTable;
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}
void transfer(HashMapEntry[] newTable) {
int newCapacity = newTable.length;
for (HashMapEntry<K,V> e : table) {
while(null != e) {
HashMapEntry<K,V> next = e.next;
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;
}
扩容代码不难,不做解释
void createEntry(int hash, K key, V value, int bucketIndex) {
HashMapEntry<K,V> e = table[bucketIndex];
table[bucketIndex] = new HashMapEntry<>(hash, key, value, e);
size++;
}
HashMapEntry(int h, K k, V v, HashMapEntry<K,V> n) {
value = v;
next = n;
key = k;
hash = h;
}
在添加HashMapEntry时,会让e指向table[bucketIndex],然后table[bucketIndex]创建新的HashMapEntry,新的HashMapEntry指向e,也就是说,HashMapEntry的添加操作将新节点加到链表头。现在,put()的整个操作就清晰了,数据结构也是如此,如下图:
看完了put() , 再看remove()操作
HashMapEntry的移除
public V remove(Object key) {
Entry<K,V> e = removeEntryForKey(key);
return (e == null ? null : e.getValue());
}
final Entry<K,V> removeEntryForKey(Object key) {
if (size == 0) {
return null;
}
//获得hash值
int hash = (key == null) ? 0 : sun.misc.Hashing.singleWordWangJenkinsHash(key);
//定位table
int i = indexFor(hash, table.length);
HashMapEntry<K,V> prev = table[i];
HashMapEntry<K,V> e = prev;
while (e != null) {
HashMapEntry<K,V> next = e.next;
Object k;
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k)))) {
modCount++;
size--;
//在链表头
if (prev == e)
table[i] = next;
//其他位置
else
prev.next = next;
e.recordRemoval(this);
return e;
}
prev = e;
e = next;
}
return e;
}
通过key来移除HashMapEntry,首先当然是获取hash值,然后定位在table上的位置,接着遍历此位置上的链表,根据HashMapEntry在链表的不同位置坐相应处理。
其他的操作如containsValue()也大同小异,都是先获取hash值,定位在table上的位置,遍历链表。在此就不多赘述了。
总结
1、HashMap以数组和链表混合作为数据结构,存储粒度为HashMapEntry
2、HashMap加载因子用以权衡在空间和时间上的权重
此处先做了解HashMap的基本构造,往后会做更新