最近做的幾個項目都是用Map來存儲的數據 ,雖然用得挺順手,但是對HashMap的底層原理卻只知甚少,今天便來簡單學習和整理一下。
數據結構中有數組和鏈表這兩個結構來存儲數據。
數組存儲區間是連續的,占用內存嚴重,故空間復雜的很大。但數組的二分查找時間復雜度小,為O(1);數組的特點是:尋址容易,插入和刪除困難;
鏈表存儲區間離散,占用內存比較寬松,故空間復雜度很小,但時間復雜度很大,達O(N)。鏈表的特點是:尋址困難,插入和刪除容易。
綜合這兩者的優點,摒棄缺點,哈希表就誕生了,既滿足了數據查找方面的特點,占用的空間也不大。


哈希表可以說就是數組鏈表,底層還是數組但是這個數組每一項就是一個鏈表。
在這個數組中,每個元素存儲的其實是一個鏈表的頭,元素的存儲位置一般情況是通過hash(key)%len獲得,也就是元素的key的哈希值對數組長度取模得到。比如上述哈希表中,12%16=12,28%16=12,108%16=12,140%16=12。所以12、28、108以及140都存儲在數組下標為12的位置。
HashMap的構造函數
HashMap實現了Map接口,繼承AbstractMap。其中Map接口定義了鍵映射到值的規則,而AbstractMap類提供 Map 接口的骨干實現。
HashMap提供了三個構造函數:
HashMap():構造一個具有默認初始容量 (16) 和默認加載因子 (0.75) 的空 HashMap。
HashMap(int initialCapacity):構造一個帶指定初始容量和默認加載因子 (0.75) 的空 HashMap。
HashMap(int initialCapacity, float loadFactor):構造一個帶指定初始容量和加載因子的空 HashMap。
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();
每次初始化HashMap都會構造一個table數組,而table數組的元素為Entry節點。
static class Entry<K,V> implements Map.Entry<K,V> { final K key; V value; Entry<K,V> next; int hash; }
HashMap也可以說是一個數組鏈表,HashMap里面有一個非常重要的內部靜態類——Entry,這個Entry非常重要,它里面包含了鍵key,值value,下一個節點next,以及hash值,Entry是HashMap非常重要的一個基礎Bean,因為所有的內容都存在Entry里面,HashMap的本質可以理解為 Entry[ ] 數組。
HashMap.put(key,value)
public V put(K key, V value) { //當key為null,調用putForNullKey方法,保存null與table第一個` 位置中,這是HashMap允許為null的原因 if (key == null) return putForNullKey(value); //計算key的hash值 int hash = hash(key.hashCode()); ------(1) //計算key hash 值在 table 數組中的位置 int i = indexFor(hash, table.length); ------(2) //從i出開始迭代 e,找到 key 保存的位置 for (Entry<K, V> e = table[i]; e != null; e = e.next) { Object k; //判斷該條鏈上是否有hash值相同的(key相同) //若存在相同,則直接覆蓋value,返回舊value if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { V oldValue = e.value; //舊值 = 新值 e.value = value; e.recordAccess(this); return oldValue; //返回舊值 } } //修改次數增加1 modCount++; //將key、value添加至i位置處 addEntry(hash, key, value, i); return null; }
我簡單的理解一下,當執行put操作的時候,HashMap會先判斷一下要存儲內容的key值是否為null,如果為null,如果為null,則執行putForNullKey方法,這個方法的作用就是將內容存儲到Entry[]數組的第一個位置,如果key不為null,則去計算key的hash值,然后對數組長度取模,得到要存儲位置的下標,再迭代該數組元素上的鏈表,看該鏈表上是否有hash值相同的,如果有hash值相同的,就直接覆蓋value的值,如果沒有hash值相同的情況,就將該內容存儲到鏈表的表頭,最先儲存的內容會放在鏈表的表尾,其實這帶代碼也順道解釋了HashMap沒有Key值相同的情況。這里還有一個情況也要說明一下,會不會出現鏈表過長的情況?隨着要存儲的內容越來越多,HashMap里面的東西也越來越多,相同下標的情況也增多,那么迭代鏈表的也無疑增加了,這會影響數據的查詢效率,HashMap對此也做了優化,當HashMap中存儲的內容超過數組長度 *loadFactor時,數組就會進行擴容,默認的數組長度是16,loadFactor為加載因子,默認的值為0.75。對於擴容需要說明的一點就是,擴容是一個非常“消耗”的過程,需要重新計算數據在新數組中的位置,並且將內容復制到新數組中,如果我們預先知道HashMap中的元素個數,預設元素的個數,能有效的提高HashMap的存儲效率。
HashMap.get(key)
public V get(Object key) { if (key == null) return getForNullKey(); Entry<K,V> entry = getEntry(key); return null == entry ? null : entry.getValue(); } 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; }
get(key)方法的代碼比較好理解,根據key的hash值找到對應的Entry即鏈表,然后在返回該key值對應的value。
HashMap的遍歷
public static void main(String[] args) {
Map<String, String> map = new HashMap<String, String>();
map.put("1", "value1");
map.put("2", "value2");
map.put("3", "value3");
//第一種:普遍使用,二次取值
System.out.println("通過Map.keySet遍歷key和value:");
for (String key : map.keySet()) {
System.out.println("key= "+ key + " and value= " + map.get(key));
}
//第二種
System.out.println("通過Map.entrySet使用iterator遍歷key和value:");
Iterator<Map.Entry<String, String>> it = map.entrySet().iterator();
while (it.hasNext()) {
Map.Entry<String, String> entry = it.next();
System.out.println("key= " + entry.getKey() + " and value= " + entry.getValue());
}
//第三種:推薦,尤其是容量大時
System.out.println("通過Map.entrySet遍歷key和value");
for (Map.Entry<String, String> entry : map.entrySet()) {
System.out.println("key= " + entry.getKey() + " and value= " + entry.getValue());
}
//第四種
System.out.println("通過Map.values()遍歷所有的value,但不能遍歷key");
for (String v : map.values()) {
System.out.println("value= " + v);
}
}
使用HashMap的匿名內部類Entry遍歷比使用keySet()效率要高很多,使用forEach循環時要注意不要在循環的過程中改變鍵值對的任何一方的值,否則出現哈希表的值沒有隨着鍵值的改變而改變,到時候在刪除的時候會出現問題。 此外,entrySet比keySet快些。對於keySet其實是遍歷了2次,一次是轉為iterator,一次就從hashmap中取出key所對於的value。而entrySet只是遍歷了第一次,他把key和value都放到了entry中,所以就快了。
