hashmap头插法是在jdk1.7之前版本中存在的。
一、什么是头插法
hashmap结构内部table表,当不同的元素hash相等的时候,在该位置table[i]形成链表,相同位置后来者插入到链表表头的位置
public V put(K key, V value) {
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
if (key == null)
return putForNullKey(value);
int hash = hash(key);
int i = indexFor(hash, table.length);
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;
}
for循环里头的意思是遇到相同的key,直接覆盖
不同的key,继续执行
addEntry(hash, key, value, i);
void addEntry(int hash, K key, V value, int bucketIndex) {
if ((size >= threshold) && (null != table[bucketIndex])) {
resize(2 * table.length);
hash = (null != key) ? hash(key) : 0;
bucketIndex = indexFor(hash, table.length);
}
createEntry(hash, key, value, bucketIndex);
}
addEntry的时候检查当前元素个数有没有达到hashmap最大容量*加载因子的值,如果达到则扩容,先不看扩容,先看看在不扩容的条件下怎么实现头插法
void createEntry(int hash, K key, V value, int bucketIndex) {
Entry<K,V> e = table[bucketIndex];
table[bucketIndex] = new Entry<>(hash, key, value, e);
size++;
}
/**
* Creates new entry.
*/
Entry(int h, K k, V v, Entry<K,V> n) {
value = v;
next = n;
key = k;
hash = h;
}
createEntry新建一个Entry,不管数组table[bucketIndex]是否为空(不管table表bucketIndex位置是否已有元素),新建的Entry的next赋值为table[bucketIndex],所以当table[bucketIndex]不为空的时候,新插入的元素永远都是链表链表头的位置。完成了任何元素的put。
这里为什么要使用头插法呢?当链表中有n个元素的时候如果往链表尾部插入一个元素,需要遍历链表,时间负责度为n,头插法时间复杂度为1。
二、扩容
hashmap初始容量默认为16,当容量不够时,会自动扩容,每次扩容新容量为当前容量的2倍
resize(2 * table.length);
void resize(int newCapacity) {
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
}
Entry[] newTable = new Entry[newCapacity];
transfer(newTable, initHashSeedAsNeeded(newCapacity));
table = newTable;
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}
创建一个新的2倍容量的空数组Entry[] newTable = new Entry[newCapacity];
/**
* Transfers all entries from current table to newTable.
*/
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;
}
}
}
table为原来的数组,需要把table的元素放到新的newTable中。
for循环表里table数组,(数组的每一个值都有可能是链表)
while遍历链表,将链表中的所有元素放到新的newTbale中,当链表元素e.next为空的时候结束当前位置链表遍历。
那有怎么保证newTable中的元素形成链表结构,并且也符合头插法的方式插入呢
e.hash 还是原来的hash,可以改变重新计算hash,默认不重新计算hash,hash不用重新计算,但是Entry e在新的newTable的位置还是要重新计算的,因为newCapacity,变成了原来的2倍,位置基本都会改变。
int i = indexFor(e.hash, newCapacity);
计算得到新的位置:i
这是不管newTable[i]是否有值
将此时的Entry e 的next赋值为newTable[i],即时
e.next = newTable[i];
然后newTable[i] = e
这就相当于在newTable[i]的链首插入进来Entry e