jdk1.7 HashMap源码初探
HashMap的数据结构
HashMap有个Entry<K,V>[] table属性用来存放最终的key-value,Entry是HashMap的内部类,table是一个Entry数组,初始是空的。只有执行第一次put的时候才会初始化Entry大小。
新建HashMap对象
HashMap共提供了四个构造方法,其中最常用的是无参构造方法,四个构造方法分别如下:
无参构造
public HashMap() {
this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
}
其中初始大小为1<<4即16,扩容因子为0.75。扩容因子用于触发map扩容,详见后文。
传入一个已有的map对象
public HashMap(Map<? extends K, ? extends V> m) {
this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1,
DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR);
inflateTable(threshold);
putAllForCreate(m);
}
指定map初始大小
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
指定map初始大小和扩容因子
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
threshold = initialCapacity;
init();
}
前面三个构造方法最终都是调用最后一个构造方法,这个构造方法只是指定了容量大小(capacity),和扩容因子(loadFactor),并对入参进行合法性校验。init方法是个空方法。此时table是空。threshold是扩容触发值,即达到这个值就触发扩容,详见后文。
put操作
完整的put方法
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;
}
第一次put
此时table是空的,所以需要初始化数组大小,大小取的是初始化时指定的扩容触发值(capacity )默认16
private void inflateTable(int toSize) {
// Find a power of 2 >= toSize
int capacity = roundUpToPowerOf2(toSize);
threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
table = new Entry[capacity];
initHashSeedAsNeeded(capacity);
}
其中roundUpToPowerOf2方法用于控制扩容大小必须是2的乘方。初始化分如下两步:
1.先新建一个指定大小的Entry(HashMap的内部类)
2.初始化hash掩码值(为后面扩容时提供判断是否需要进行重新hash,没有细看)
初始化之后获取key的hash值,然后再根据hash值获取要存放该key-value的下标
int hash = hash(key);
int i = indexFor(hash, table.length);
如果key是null,会调用putForNullKey方法插入key为null的Entry。所以HashMap的key是可以为null的
private V putForNullKey(V value) {
for (Entry<K,V> e = table[0]; e != null; e = e.next) {
if (e.key == null) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
addEntry(0, null, value, 0);
return null;
}
然后调用addEntry方法插入数据(由于是第一次put,所以Entry<K,V> e = table[i] 是null,不会进入for操作)
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);
}
由于第一次put,无需扩容,所有直接调用createEntry创建Entry
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++;
}
说白了就是new了一个Entry对象并放入下标为刚才获取的下标下,看下Entry的构造函数
final K key;
V value;
Entry<K,V> next;
int hash;
Entry(int h, K k, V v, Entry<K,V> n) {
value = v;
next = n;
key = k;
hash = h;
}
很简单,就四个属性,除了key,value外,还有一个hash值,以及它的nextEntry,说明这是一个链表结构。
至此第一个元素就已经put好了
无需扩容
大致同第一次put,只是少了一开始的初始化数组大小的操作。另外,如果这时put的key的hash值后获取到的下标所在元素已经存在了,则需要进入刚才没有进去的for循环进行替换操作
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;
}
}
这段主要是判断key是否存在,如果存在则替换value值。判断依据是1.是否是同一个对象,2.调用key的equals方法,返回是否为true。
如果key不存在,则继续调用createEntry添加entry。如果该小标已经存在对象,则将原来存在的Entry放到自己Entry的next的属性里,即插入了链表的头部(头插法)。
需要扩容
大致同上,主要区别在于addEntry方法里的if语句块里
if ((size >= threshold) && (null != table[bucketIndex])) {
resize(2 * table.length);
hash = (null != key) ? hash(key) : 0;
bucketIndex = indexFor(hash, table.length);
}
由此可见,HashMap扩容的触发条件是,当前已经插入的对象数量超过了需要扩容的数量(threshold),且要插入的下标有值。我之前会疑惑如果别的元素都插了好多了就这个下标没元素,这样岂不很不科学?其实key的hash值是能够保证正态分布的,无须担心。扩容完成后需要对key进行重新hash并获取新的下标(毕竟数组大小变了),其余同上。下面详细介绍扩容逻辑
扩容
resize方法
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);
}
resize大致有如下几个步骤:
1.首先判断已有的table数组大小有没有超过int的最大值(2^31),所以HashMap是不能无限制存东西的。
2.然后再新建一个新的大小的数组(原有大小的两倍)。说白了就是扩大了一倍。
3.然后调用transfer方法对已有的Entry数组进行重新hash(因为数组大小不一样了,所以同一个key的hash值也不一样了,所以要重新hash,不然就get不到了)
4.最后将重新散列后的entry数组赋给table,并重新修改扩容触发阈值
接下来详细说下transfer方法:
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;
}
}
}
transfer方法也很简单,遍历table中的每个Entry,然后再遍历每个Entry的链表。然后对链表上的每一个Entry的key进行重新hash,存入新的table中。
get操作
final Entry<K,V> getEntry(Object key) {
if (size == 0) {
return null;
}
int hash = (key == null) ? 0 : hash(key);
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;
}
根据key的hash值获取table下标,然后再遍历该链表的每一个Entry,如果key相同则返回。(极端情况下,如果链表太长,会影响get的效率,毕竟遍历链表用的for循环啊。所以要设置合理的扩容因子,默认的0.75一般还是不要改的好)
jdk1.7扩容造成死循环分析
假设一个map有如下几个Entry
扩容之后table数组大小变成4,两个线程同时执行扩容操作,线程一执行完transfer方法的e.next地方时被挂起,此时next是key7
此时线程二执行完整个扩容操作,扩容完成后的table结构如下
线程一继续执行,此时e是key:3,next=e.next=null(因为线程2已经完成了重新hash,将key3的next置为null,如图)。
线程1开始运行,继续进行扩容
1.此时e是key3,next是key7(刚才已经执行到这然后被挂起的)。线程1新建的newTable与线程2新建的newTable不是同一个对象,所以此时线程1新建的newTable下标为3的元素还是空的。继续下面的逻辑,将key3头插到下标为3的table中,key3的next为空(因为此时newTable下标为3的元素还是空),由于next是key7不是null,继续循环。
2.此时e是key7,由于线程2已经把key7的next置为了key3,所以此时next是key3,继续下面的逻辑,将key7头插到小标为3的table中,key7的next被线程1又置一次为key3,由于next是key3不为空,继续循环。
3.此时e是key3,next为空(见第一步),将key3头插到下标为3的元素中,key3的next被置为key7.但此时的next是null,所以循环结束,此时线程1的tabla结构如下
当get一个key时,如果该key被hash到下标为3这个元素,且不存在于map中,比如key15,程序就会遍历下标为3的Entry链表,先找到key3,然后遍历key3的next key7,然后再找key7的next key3,由于一直找不到key15,所以就会一直循环下去,就陷入了死循环。