想了解HashMap的原因是,前幾天去某公司面試,被問到這個,我一臉懵逼,所以決定回來補補知識。
- 數據結構基礎操作執行性能
- 數組:採用一段連續的存儲單元存儲數據。對於查找指定數字的下標,時間複雜度是O(1)。對於查找關鍵字是否在數組裏面,時間複雜度是O(n)。對於插入刪除,時間複雜度是O(n)。
- 線性鏈表:對於鏈表的新增刪除等操作,時間複雜度爲O(1)。而查找操作時間複雜度是O(n)。
- 二叉樹:對於相對平衡的有序二叉樹,對其進行插入查找刪除的操作,平均複雜度是O(logn)。
- 哈希表:哈希表中進行添加,刪除,查找的操作性能十分之高,不考慮哈希衝突的情況下,只需一次定位即可完成。時間複雜度爲O(1)。
- 數據結構的物理存儲只有兩種:順序存儲和鏈式存儲。
- 哈希表的主幹就是數組。
- 我們要新增或查找某個元素,通過把當前元素的關鍵字通過某函數映射到數組中的某個位置。這個函數被稱爲哈希函數,這個函數的好壞直接影響哈希表的優劣。
- 哈希衝突:當我們對某個元素進行哈希運算,得到一個存儲值,,在進行插入的時候發現地址被佔用了,那麼就發生了所謂的哈希衝突。好的哈希函數可以保證計算簡單和散列地址分佈均勻。
- 哈希衝突的解決方法:開放定址法,再散列函數法,鏈地址法。而HashMap採用鏈地址法。也就是HashMap使用的是數組+鏈表。
- HashMap的實現原理:HashMap的主幹是一個Entry數組。Entry是HashMap的基本組成元素。每個Entry包含一個key-value鍵值對。初始值是空數組{}。主幹數組的長度一定是2的次冪。
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;
- Entry是HashMap的靜態內部類。
static class Entry<K,V> implements Map.Entry<K,V> {
final K key;
V value;
Entry<K,V> next;//存儲指向下一個Entry的引用,單鏈表結構
int hash;//對key的hashcode值進行hash運算後得到的值,存儲在Entry,避免重複計算
/**
* Creates new entry.
*/
Entry(int h, K k, V v, Entry<K,V> n) {
value = v;
next = n;
key = k;
hash = h;
}
則HashMap的整體結構
8. 簡單來說,HashMap是由數組+鏈表組成。數組是HashMap的主體,鏈表是爲了解決哈希衝突。如果定位到的數組不包含鏈表,那麼查找和添加操作就很快,僅需一次尋址。如果定位到的數組包含鏈表,對於添加操作就是時間複雜度O(n),首先遍歷數組,存在就覆蓋,不存在就新加。對於查找操作,仍需遍歷鏈表,通過key對象的equals方法逐一比對查找。所以性能考慮,HashMap中的鏈表出現越少,性能越好。
9. HashMap類中其他幾個重要的數據結構
- transient int size:實際存儲key-value鍵值對的個數
- int threshold:閥值,當table={}時,這個值是初始值16。當table被填充,也就是爲table分配內存空間之後,threshold一般爲capacity*loadFactor。HashMap在進行擴容的時候回考慮threshold。
- final float loadFactor:負載因子,代表了HashMap的填充度有多少,一般是0.75。
- transient int modCount:用於快速失敗,由於HashMap非線程安全,在對HashMap進行線程迭代的時候,如果其他線程的參與導致HashMap結構發生變化(比如:put,remove等操作),需要拋出異常ConcurrentModificationException。
- HashMap有四個構造器:
- HashMap():構造一個空的HashMap,默認初始容量是16,負載因子是0.75。
- HashMap(int initialCapacity):構造一個空的HashMap,初始容量是initialCapacity,負載因子是0.75。
- HashMap(int initialCapacity,float loadFactor):構造一個空的HashMap,初始容量是initialCapacity,負載因子是loadFactor。
- HashMap(Map<? extends K,? extends V> m):用相同的Map構造一個新的HashMap。
- 看一下上面的第三個構造函數的代碼。
public HashMap(int initialCapacity, float loadFactor) {
//此處對傳入的初始容量進行校驗,最大不能超過MAXIMUM_CAPACITY = 1<<30(230)
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();//init方法在HashMap中沒有實際實現,不過在其子類如 linkedHashMap中就會有對應實現
}
在這裏可以看到,在常規構造器中,沒有爲數組table分配內存空間(除了第四個構造器)。而是在執行put操作的時候才真正構建table數組。
12. 在瞭解put操作之前,我們先了解一下inflateTable方法。
private void inflateTable(int toSize) {
int capacity = roundUpToPowerOf2(toSize);//capacity一定是2的次冪
threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);//此處爲threshold賦值,取capacity*loadFactor和MAXIMUM_CAPACITY+1的最小值,capaticy一定不會超過MAXIMUM_CAPACITY,除非loadFactor大於1
table = new Entry[capacity];
initHashSeedAsNeeded(capacity);
}
inflateTable方法用於給主幹數組table分配內存空間,通過roundUpToPowerOf2(toSize)可以確保capacity爲大於或等於toSize的最接近toSize的二次冪。如果toSize=13,那麼capacity=16。如果toSize=20,那麼capacity=32。
13. 接着瞭解一下,hash()函數。
final int hash(Object k) {
int h = hashSeed;
if (0 != h && k instanceof String) {
return sun.misc.Hashing.stringHash32((String) k);
}
h ^= k.hashCode();
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
用了很多的異或,移位等運算,對key的hashcode進一步進行計算,以及二進制位的調整等來保證最終獲取的存儲位置儘量分佈均勻。
14. 當用hash()函數計算出了值之後,通過indexFor()進一步處理來獲取實際存儲位置。
static int indexFor(int h, int length) {
return h & (length-1);
}
h & (length -1 )可以保證最終獲得的下標一定在數組範圍內。舉例:h = 23,length = 14,length-1 = 13。
1 0 1 1 1
& 0 1 1 0 1
——————————————
0 0 1 0 1 =5
則最後在數組中的下標是5。當然這也可以用模運算來實現,但是位運算對計算機來說性能更高。
15. addEntry()的實現:
void addEntry(int hash, K key, V value, int bucketIndex) {
if ((size >= threshold) && (null != table[bucketIndex])) {
resize(2 * table.length);//當size超過臨界閾值threshold,並且即將發生哈希衝突時進行擴容
hash = (null != key) ? hash(key) : 0;
bucketIndex = indexFor(hash, table.length);
}
createEntry(hash, key, value, bucketIndex);
}
當size大於閥值或者發生哈希衝突的時候,需要進行數組擴容。擴容時需要新建一個長度爲之前數組2倍的新數組,然後將當前的Entry數組全部傳輸過去
16. 最後瞭解一下put()方法
public V put(K key, V value) {
//如果table數組爲空數組{},進行數組填充(爲table分配實際內存空間),入參爲threshold,此時threshold爲initialCapacity 默認是1<<4(24=16)
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
//如果key爲null,存儲位置爲table[0]或table[0]的衝突鏈上
if (key == null)
return putForNullKey(value);
int hash = hash(key);//對key的hashcode進一步計算,確保散列均勻
int i = indexFor(hash, table.length);//獲取在table中的實際位置
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
//如果該對應數據已存在,執行覆蓋操作。用新value替換舊value,並返回舊value
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++;//保證併發訪問時,若HashMap內部結構發生變化,快速響應失敗
addEntry(hash, key, value, i);//新增一個entry
return null;
}
- 關於HashMap的數組長度爲何一定是2的次冪。
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);
}
如果數組發生可擴容,那麼數組長度發生變化,存儲位置index = h & (length-1)的肯定會發生變化。需要重新計算index。
再看看transfer方法。
void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
//for循環中的代碼,逐個遍歷鏈表,重新計算索引位置,將老數組數據複製到新數組中去(數組不存儲實際數據,所以僅僅是拷貝引用而已)
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);
//將當前entry的next鏈指向新的索引位置,newTable[i]有可能爲空,有可能也是個entry鏈,如果是entry鏈,直接在鏈表頭部插入。
e.next = newTable[i];
newTable[i] = e;
e = next;
}
}
}
數組長度一定是2的次冪是因爲,爲了保持新的數組索引和老的數組索引的大致上的一致。
舉例:h = 21, length - 1 = 15 ,new_length - 1 = 31。
1 0 1 0 1
& 0 1 1 1 1
————————
0 0 1 0 1 =5
0 1 0 1 0 1
& 0 1 1 1 1 1
————————
0 1 0 1 0 1 =21
當length-1低位全爲1的情況下,h的低位就只有一種情況。這樣就避免了之前散列的很好的老數組的數據位置重新調換。
- get()方法:
public V get(Object key) {
//如果key爲null,則直接去table[0]處去檢索即可。
if (key == null)
return getForNullKey();
Entry<K,V> entry = getEntry(key);
return null == entry ? null : entry.getValue();
}
getEntry()方法:
final Entry<K,V> getEntry(Object key) {
if (size == 0) {
return null;
}
//通過key的hashcode值計算hash值
int hash = (key == null) ? 0 : hash(key);
//indexFor (hash&length-1) 獲取最終數組索引,然後遍歷鏈表,通過equals方法比對找出對應記錄
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;
}
- 寫equals()方法時需要重寫hashCode()方法
轉載自
作者: dreamcatcher-cx
出處: http://www.cnblogs.com/chengxiao/