一、创建一个HashMap都做了哪些工作?
Map<String,String> map = new HashMap<String,String>();
HahMap无参构造方法
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR;
threshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR);
table = new Entry[DEFAULT_INITIAL_CAPACITY];
init();
}
可以看到设置了加载因子(默认0.75)、阈值(默认容量16*默认加载因子0.75=12)、table是HashMap内部数据存储结构Entry数组。当HashMap的size大于等于阈值时,HashMap会扩容到原来的容量的2倍。而如果 new HashMap(1),它的容量是不是1呢?可以看到最终调用的构造方法:
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}:
最终调用的是下面的
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);
// Find a power of 2 >= initialCapacity
int capacity = 1;
while (capacity < initialCapacity)
capacity <<= 1;
this.loadFactor = loadFactor;
threshold = (int)(capacity * loadFactor);
table = new Entry[capacity];
init();
}
可以看到HashMap的size总是2的倍数,即new HashMap(1)的最终容量是2.
Entry是什么呢?Entry就是Map中存放的key-value对,并且保存了下一个节点的引用next和key的hash值,也就实现了链表的功能。
Entry结构体:
HashMap存储结构图:
二、put(key,value)的实现
public V put(K key, V value) {
if (key == null)
return putForNullKey(value);
int hash = hash(key.hashCode());
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;
}
HashMap允许存null key值,它会把key为null放到table[0]的位置,即第一个bucket(table数组中的元素称为bucket桶)。
添加一个新元素时,先根据key值计算出hash码,根据hash码计算出所在bucket的位置i。遍历table[i]下挂的entry,如果key值存在就覆盖value,不存在就添加新的entry。
void addEntry(int hash, K key, V value, int bucketIndex) {
Entry<K,V> e = table[bucketIndex];
table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
if (size++ >= threshold)
resize(2 * table.length);
}
Entry(int h, K k, V v, Entry<K,V> n) {
value = v;
next = n;
key = k;
hash = h;
}
new Entry()会把当前的Entry作为新创建的Entry的下一个结点链接起来。
addEntry方法中判断容量是否超过阈值,若超过会扩容到原来的2倍大小。
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);
table = newTable;
threshold = (int)(newCapacity * loadFactor);
}
扩容之后把原来的元素转移过来,重新hash,重新分配bucket位置,同一个bucket链接的entry会倒序链接起来。
void transfer(Entry[] newTable) {
Entry[] src = table;
int newCapacity = newTable.length;
for (int j = 0; j < src.length; j++) {
Entry<K,V> e = src[j];
if (e != null) {
src[j] = null;
do {
Entry<K,V> next = e.next;
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;
} while (e != null);
}
}
}
三、get(key)的实现
get就比较简单,按照hash码找到对应的bucket,遍历挂载的entry,若key值相同就返回对应的value,若找不到就返回null。
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;
}
四、hash冲突处理方法
1、开放地址法
2、链地址法
HashMap处理hash冲突采用的链地址法