Map接口主要藉助了hash的思想,以hash表鍵值對的形式存儲,鍵用於hash定位,具有極高的效率。其接口主要實現類如下:
├Hashtable(基本同hashMap,默認爲11,只不過hashtable爲線程安全的,不允許有null值,put, get 都加鎖)
├HashMap(Entry鏈表+數組,默認容量爲16,負載因子爲0.75;長度大於n*16*0.75則容量增大一倍)
Map底層數據結構爲哈希表,用Entry數組表示,Entry數據結構如下:
private static class Entry<K,V> implements Map.Entry<K,V> {
int hash;
K key;
V value;
Entry<K,V> next;
protected Entry(int hash, K key, V value, Entry<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
...
}
Entry數組存儲示意圖如下:
在存儲entry時,根據key的hash值定位到Entry數組的相應位置,如果該位置沒有元素則直接存入,若該位置有元素則將entry鏈接至以該位置元素爲表頭的鏈表(JDK1.8中此處做了優化,當鏈表長度超過一定長度時會轉變爲紅黑樹存儲)。在Entry同一個位置的entry具有相同的key哈希值。在查找相應的元素時,首先計算該entry對象key屬性的哈希值,然後根據其hash值定位到Entry數組相應的位置,然後遍歷鏈表並比較entry對象,直到找到相等值。
Entry數組(哈希表、散列表)的容量是可變的,在初始化時有初始化大小initialCapacity 和負載因子load factor。當數組容量達到Entry.leng*load factor時,會重新分配一個容量爲原來兩倍的Entry數組,並將原來Entry數組中的元素重新hash至新的Entry數組。負載因子是時間和空間上的一種折中,負載因子越大表示散列表填充程度越大,反之越小。散列表填充程度越大,發生元素碰撞的概率越大,鏈表長度也就越長,查找元素時也就越慢。增大負載因子可以減少散列表所佔空間,但會增加查詢數據的時間開銷(put/get均會用到查詢);減小負載因子會提高數據查詢的性能,但會增加散列表所佔用的存儲空間。
一般情況下load factor默認爲0.75,可以根據 實際需要適當地調整 load factor 的值;如果程序比較關心空間開銷、內存比較緊張,可以適當地增加負載因子;如果程序比較關心時間開銷,內存比較寬裕則可以適當的減少負載因子。通常情況下,程序員無需改變負載因子的值。
1、Hashtable繼承自抽象類Dictionary,並實現Map接口
默認大小爲11、線程安全(對Entry數組的操作加鎖synchronized)、key-value不允許爲null、擴容爲2*n+1、散列方法是(hash & 0x7FFFFFFF) % tab.length
a、put操作
先根據entry的key哈希值定位到散列表的相應位置,如果該位置具有相同key的元素直接覆蓋,如果散列表達到容量極限需要擴容並重新哈希原來的散列表,最後把待插入的entry放入的到相應的位置。
其源碼如下:
public synchronized V put(K key, V value) {
// Make sure the value is not null
if (value == null) {
throw new NullPointerException();
}
// Makes sure the key is not already in the hashtable.
Entry tab[] = table;
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length;//根據key值哈希定位
for (Entry<K,V> e = tab[index] ; e != null ; e = e.next) {
if ((e.hash == hash) && e.key.equals(key)) {//key已經存在則直接覆蓋
V old = e.value;
e.value = value;
return old;
}
}
modCount++;
if (count >= threshold) {//容量達到閾值則擴容極限重新哈希
// Rehash the table if the threshold is exceeded
rehash();
tab = table;
index = (hash & 0x7FFFFFFF) % tab.length;
}
// Creates the new entry.
Entry<K,V> e = tab[index];//取出表頭元素
tab[index] = new Entry<K,V>(hash, key, value, e);//將元素插入作爲新的表頭
count++;
return null;
}
hastable擴容:
protected void rehash() {
int oldCapacity = table.length;
Entry[] oldMap = table;//老的散列表
int newCapacity = oldCapacity * 2 + 1;//爲保證散列效果,表長度爲奇數
Entry[] newMap = new Entry[newCapacity];.//新的散列表,容量爲原來的兩倍
modCount++;
threshold = (int)(newCapacity * loadFactor);//擴容閾值
table = newMap;//原來的table引用指向新的散列表
//重新hash老的散列表,並將其插入到新的散列表中
for (int i = oldCapacity ; i-- > 0 ;) {
for (Entry<K,V> old = oldMap[i] ; old != null ; ) {
Entry<K,V> e = old;
old = old.next;
int index = (e.hash & 0x7FFFFFFF) % newCapacity;
e.next = newMap[index];
newMap[index] = e;
}
}
}
原來的散列表仍然保存,能夠保證在擴容時,其他線程正常訪問散列表。
b、get操作
先根據key定位到相應的列表,然後遍歷列表,找不到返回null
public synchronized V get(Object key) {
Entry tab[] = table;
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length;//根據key的哈希值定位
for (Entry<K,V> e = tab[index] ; e != null ; e = e.next) {
if ((e.hash == hash) && e.key.equals(key)) {//遍歷列表並比較
return e.value;
}
}
return null;
}
2、HashMap繼承自抽象類AbstractMap,並實現Map接口
默認大小爲16、線程非安全、允許key-value爲null、擴容爲2^n、二次散列hash&(length-1)
初始化大小爲第一個大於給定值並且爲2^n的整數,如果給定大小爲20,那麼初始化大小爲32。初始化源碼如下:
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找到第一個大於給定值並且爲2^n的整數,爲了便於散列,且在定位時低位跟1做位與
int capacity = 1;
while (capacity < initialCapacity)
capacity <<= 1;
this.loadFactor = loadFactor;
threshold = (int)(capacity * loadFactor);
table = new Entry[capacity];
init(); //回調函數,子類實現
}
a、put操作
跟hashtable操作基本類似,散列方式不同、擴容方式不同。根據key的hash值找到散列表中的索引後,會循環遍歷table[i]所在鏈表,若找到已存在key值則直接覆蓋,如不存在則通過addEntry添加新對象至鏈表頭部。
public V put(K key, V value) {
if (key == null)
return putForNullKey(value);
int hash = hash(key.hashCode());//二次散列使得散列更加均勻
int i = indexFor(hash, table.length);//根據散列定位
//若i處索引不爲null,通過循環不斷遍歷e的下一個元素
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
//有相同key則覆蓋
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);//回調函數,子類實現
return oldValue;
}
}
//能執行到此處,說明兩點1、i處索引爲空,2、遍歷完鏈表沒有找到與key相同的值
modCount++;
addEntry(hash, key, value, i);//將key、value 添加到索引i處
return null;
}
二次散列:
static int hash(int h) {
// This function ensures that hashCodes that differ only by
// constant multiples at each bit position have a bounded
// number of collisions (approximately 8 at default load factor).
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
根據哈希值找到索引:
static int indexFor(int h, int length) {
return h & (length-1);//h每一位跟1做與操作,極快
}
添加元素,先添加再擴容
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);//擴容爲原來的兩倍
}
擴容:
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:
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;//原來的散列表直接賦值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);
}
}
}
如果在resize過程中,有其他線程視圖調用get遍歷數據會在成錯誤。
int oldCapacity = table.length;
int newCapacity = oldCapacity * 2 + 1;
Hashtable的尋址是這樣做的:
Entrytab[]=table;inthash=key.hashCode();intindex=(hash&0x7FFFFFFF)%tab.length;