首先想說的是關於HashMap源碼的分析園子里面應該有很多,並且都是分析得很不錯的文章,但是我還是想寫出自己的學習總結,以便加深自己的理解,因此就有了此文,另外因為小孩過來了,因此更新速度可能放緩了,(#^.^#)
一、HashMap的簡單使用
學習任何一個集合,首先最基本的是學會使用,因此首先我們看下如何使用HashMap,以及我們經常使用的方法又有哪些,代碼如下:
package study.collection; import java.util.HashMap; import java.util.Map; public class TestMap { public static void main(String[] args) { Map map = new HashMap(); map.put("張三", new Wife("六亦菲")); map.put("李四",new Wife("楊咪")); map.put("王五",new Wife("趙麗英")); System.out.println(map.get("張三")); System.out.println(map.containsKey("王五")); map.remove("李四"); System.out.println(map.size()); } } class Wife { private String name; public Wife() { } public Wife(String name) { super(); this.name = name; } @Override public String toString() { return "Wife [name=" + name + "]"; } }
上面主要演示了hashmap 中的幾個主要方法,如put() get() remove() size() 等方法,實際map中還有很多的方法,如下:
說明:圓形 -- 對外提供的public 方法 正方形 ---內部使用的private 方法 三角形 --- static 方法。
二、HashMap的概述
上面我們介紹了HashMap的基本使用方法,下面我們進一步來分析下HashMap,而不在僅僅在於會使用。
2.1 HashMap的定義
HashMap基於哈希表的 Map 接口的實現。此實現提供所有可選的映射操作,並允許使用 null 值和 null 鍵。(除了不同步和允許使用 null 之外,HashMap 類與 Hashtable 大致相同.)此類不保證映射的順序,特別是它不保證該順序恆久不變。值得注意的是HashMap不是線程安全的,如果想要線程安全的HashMap,可以通過Collections類的靜態方法synchronizedMap獲得線程安全的HashMap。
Map map = Collections.synchronizedMap(new HashMap());
我們來看下Map的類定義:
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable
從上面的HashMap 此類的定義來看,我可以這樣來理解HashMap; 它實現了Map接口,繼承了AbstractMap,其中Map接口定義了鍵映射到值得規則,而AbstractMap類提供了Map接口中主要的實現,是為了最大限度的減少實際開發人員在實現HashMap所需要的工作。
2.2 HashMap的構造函數
HashMap提供了四個構造函數:
HashMap():構造一個具有默認初始容量 (16) 和默認加載因子 (0.75) 的空 HashMap。
HashMap(int initialCapacity):構造一個帶指定初始容量和默認加載因子 (0.75) 的空 HashMap。
HashMap(int initialCapacity, float loadFactor):構造一個帶指定初始容量和加載因子的空 HashMap。
HashMap(Map<? extends K,? extends V> m) :構造一個映射關系與指定 Map 相同的新 HashMap。
在這里提到了兩個參數:初始容量,加載因子。這兩個參數是影響HashMap性能的重要參數,其中容量表示哈希表中桶的數量,初始容量是創建哈希表時的容量,加載因子是哈希表在其容量擴容之前可以達到多滿的一種尺度【閥值=加載因子*容量】,加載因子它衡量的是一個散列表的空間的使用程度,加載因子越大表示散列表的裝填程度越高,反之愈小。對於使用鏈表法的散列表來說,查找一個元素的平均時間是O(1+a),因此如果加載因子越大,對空間的利用更充分,然而后果是查找效率的降低;如果加載因子太小,那么散列表的數據將過於稀疏,對空間造成嚴重浪費。系統默認加載因子為0.75,一般情況下我們是無需修改的。
HashMap是一種支持快速存取的數據結構,要了解它的性能必須要了解它的數據結構。
2.3 HashMap的數據結構
我們知道在Java中最常用的兩種結構是數組和模擬指針(引用),幾乎所有的數據結構都可以利用這兩種來組合實現,HashMap也是如此,其底層為數組+鏈表實現;如果采用數組那么必然就涉及到數組的索引的問題,那么這個索引是什么呢?就是通過key的hashcode計算出來的,采用數組的好處就在於可以快速的訪問,可以回憶下List集合中ArrayList.就是典型的底層為數組,所以訪問的速度很快,但是如果單純的采用數組,那么為什么hashmap 又有鏈表結構呢?這個是因為數組的缺陷是插入元素和刪除元素比較慢,而鏈表具備的就是插入和刪除快的特點,因此為了結合這兩種特性於是就采用了數組加鏈表的結構進行。我們看下源碼就可以看出:
public HashMap(int initialCapacity, float loadFactor) { //初始容量不能<0 if (initialCapacity < 0) throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity); //初始容量不能 > 最大容量值,HashMap的最大容量值為2^30 if (initialCapacity > MAXIMUM_CAPACITY) initialCapacity = MAXIMUM_CAPACITY; //負載因子不能 < 0 if (loadFactor <= 0 || Float.isNaN(loadFactor)) throw new IllegalArgumentException("Illegal load factor: " + loadFactor); // 計算出大於 initialCapacity 的最小的 2 的 n 次方值。 int capacity = 1; while (capacity < initialCapacity) capacity <<= 1; this.loadFactor = loadFactor; //設置HashMap的容量極限,當HashMap的容量達到該極限時就會進行擴容操作 threshold = (int) (capacity * loadFactor); //初始化table數組 table = new Entry[capacity]; init(); }
從源碼中可以看出,每次新建一個HashMap時,都會初始化一個table數組。table數組的元素為Entry節點。
static class Entry<K,V> implements Map.Entry<K,V> { final K key; V value; Entry<K,V> next; final int hash; /** * Creates new entry. */ Entry(int h, K k, V v, Entry<K,V> n) { value = v; next = n; key = k; hash = h; } ....... }
其中Entry為HashMap的內部類,它包含了鍵key、值value、下一個節點next,以及hash值,這是非常重要的,正是由於Entry才構成了table數組的項為鏈表。
總結:
HashMap的底層主要是基於數組和鏈表來實現的,它之所以有相當快的查詢速度主要是因為它是通過計算散列碼來決定存儲的位置。HashMap中主要是通過key的hashCode來計算hash值的,只要hashCode相同,計算出來的hash值就一樣。如果存儲的對象對多了,就有可能不同的對象所算出來的hash值是相同的,這就出現了所謂的hash沖突。學過數據結構的同學都知道,解決hash沖突的方法有很多,HashMap底層是通過鏈表來解決hash沖突的。
圖中,0~15部分即代表哈希表,也稱為哈希數組,數組的每個元素都是一個單鏈表的頭節點,鏈表是用來解決沖突的,如果不同的key映射到了數組的同一位置處,就將其放入單鏈表中。
從上圖我們可以發現哈希表是由數組+鏈表組成的,一個長度為16的數組中,每個元素存儲的是一個鏈表的頭結點Bucket桶。那么這些元素是按照什么樣的規則存儲到數組中呢。一般情況是通過hash(key)%len獲得,也就是元素的key的哈希值對數組長度取模得到。比如上述哈希表中,12%16=12,28%16=12,108%16=12,140%16=12。所以12、28、108以及140都存儲在數組下標為12的位置。
HashMap其實也是一個線性的數組實現的,所以可以理解為其存儲數據的容器就是一個線性數組。這可能讓我們很不解,一個線性的數組怎么實現按鍵值對來存取數據呢?這里HashMap有做一些處理。
HashMap里面實現一個靜態內部類Entry【可以看上面的源碼】,其重要的屬性有 key , value, next【單鏈表】,從屬性key,value我們就能很明顯的看出來Entry就是HashMap鍵值對實現的一個基礎bean,我們上面說到HashMap的基礎就是一個線性數組,這個數組就是Entry[],Map里面的內容都保存在Entry[]里面。即HashMap其實就是一個Entry數組,Entry對象中包含了鍵和值,其中next也是一個Entry對象,它就是用來處理hash沖突的,形成一個鏈表。
說明:鏈表結構中如果存放的數據太多,JDK1.8會使用紅黑樹存儲來提高查找(get)性能.
三、HashMap的源碼解析
我們先重點分析HashMap的兩個重要方法:put 和 get方法,也是我們最常使用的方法:
public V put(K key, V value) { // 若“key為null”,則將該鍵值對添加到table[0]中。 if (key == null) return putForNullKey(value); // 若“key不為null”,則計算該key的哈希值,然后將其添加到該哈希值對應的鏈表中。 int hash = hash(key.hashCode()); //搜索指定hash值在對應table中的索引 int i = indexFor(hash, table.length); // 循環遍歷Entry數組,若“該key”對應的鍵值對已經存在,則用新的value取代舊的value。然后退出! 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))) { //如果key相同則覆蓋並返回舊值 V oldValue = e.value; e.value = value; e.recordAccess(this); return oldValue; } } //修改次數+1 modCount++; //將key-value添加到table[i]處 addEntry(hash, key, value, i); return null; }
上面程序中用到了一個重要的內部接口:Map.Entry,每個 Map.Entry 其實就是一個 key-value 對。從上面程序中可以看出:當系統決定存儲 HashMap 中的 key-value 對時,完全沒有考慮 Entry 中的 value,僅僅只是根據 key 來計算並決定每個 Entry 的存儲位置。這也說明了前面的結論:我們完全可以把 Map 集合中的 value 當成 key 的附屬,當系統決定了 key 的存儲位置之后,value 隨之保存在那里即可。
我們慢慢的來分析這個函數,第2和3行的作用就是處理key值為null的情況,我們看看putForNullKey(value)方法:
private V putForNullKey(V value) { for (Entry<K,V> e = table[0]; e != null; e = e.next) { if (e.key == null) { //如果有key為null的對象存在,則覆蓋掉 V oldValue = e.value; e.value = value; e.recordAccess(this); return oldValue; } } modCount++; addEntry(0, null, value, 0); //如果鍵為null的話,則hash值為0 return null; }
注意:如果key為null的話,hash值為0,對象存儲在數組中索引為0的位置。即table[0]
我們再回去看看put方法中第4行,它是通過key的hashCode值計算hash碼,下面是計算hash碼的函數:
//計算hash值的方法 通過鍵的hashCode來計算 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); }
得到hash碼之后就會通過hash碼去計算出應該存儲在數組中的索引,計算索引的函數如下:
static int indexFor(int h, int length) { //根據hash值和數組長度算出索引值 return h & (length-1); //這里不能隨便算取,用hash&(length-1)是有原因的,這樣可以確保算出來的索引是在數組大小范圍內,不會超出 }
這個我們要重點說下,我們一般對哈希表的散列很自然地會想到用hash值對length取模(即除法散列法),Hashtable中也是這樣實現的,這種方法基本能保證元素在哈希表中散列的比較均勻,但取模會用到除法運算,效率很低,HashMap中則通過h&(length-1)的方法來代替取模。
h&(length - 1),這句話除了上面的取模運算外還有一個非常重要的責任:均勻分布table數據和充分利用空間。
接下來,我們分析下為什么哈希表的容量一定要是2的整數次冪。首先,length為2的整數次冪的話,h&(length-1)就相當於對length取模,這樣便保證了散列的均勻,同時也提升了效率;其次,length為2的整數次冪的話,為偶數,這樣length-1為奇數,奇數的最后一位是1,這樣便保證了h&(length-1)的最后一位可能為0,也可能為1(這取決於h的值),即與后的結果可能為偶數,也可能為奇數,這樣便可以保證散列的均勻性,而如果length為奇數的話,很明顯length-1為偶數,它的最后一位是0,這樣h&(length-1)的最后一位肯定為0,即只能為偶數,這樣任何hash值都只會被散列到數組的偶數下標位置上,這便浪費了近一半的空間,因此,length取2的整數次冪,是為了使不同hash值發生碰撞的概率較小,這樣就能使元素在哈希表中均勻地散列。
舉例:這里我們假設length為16(2^n)和15,h為5、6、7。
當length=15時,6和7的結果一樣,這樣表示他們在table存儲的位置是相同的,也就是產生了碰撞,6、7就會在一個位置形成鏈表,這樣就會導致查詢速度降低。誠然這里只分析三個數字不是很多,那么我們就看0-15。
從上面的圖表中我們看到總共發生了8此碰撞,同時發現浪費的空間非常大,有1、3、5、7、9、11、13、15處沒有記錄,也就是沒有存放數據。這是因為他們在與14進行&運算時,得到的結果最后一位永遠都是0,即0001、0011、0101、0111、1001、1011、1101、1111位置處是不可能存儲數據的,空間減少,進一步增加碰撞幾率,這樣就會導致查詢速度慢。而當length = 16時,length – 1 = 15 即1111,那么進行低位&運算時,值總是與原來hash值相同,而進行高位運算時,其值等於其低位值。所以說當length = 2^n時,不同的hash值發生碰撞的概率比較小,這樣就會使得數據在table數組中分布較均勻,查詢速度也較快。
根據上面 put 方法的源代碼可以看出,當程序試圖將一個key-value對放入HashMap中時,程序首先根據該 key 的 hashCode() 返回值決定該 Entry 的存儲位置:
- 如果兩個 Entry 的 key 的 hashCode() 返回值相同,那它們的存儲位置相同。
- 如果這兩個 Entry 的 key 通過 equals 比較返回 true,新添加 Entry 的 value 將覆蓋集合中原有 Entry 的 value,但key不會覆蓋。
- 如果這兩個 Entry 的 key 通過 equals 比較返回 false,新添加的 Entry 將與集合中原有 Entry 形成 Entry 鏈,而且新添加的 Entry 位於 Entry 鏈的頭部
具體說明繼續看 addEntry() 方法的說明。
void addEntry(int hash, K key, V value, int bucketIndex) { Entry<K,V> e = table[bucketIndex]; //如果要加入的位置有值,將該位置原先的值設置為新entry的next,也就是新entry鏈表的下一個節點 table[bucketIndex] = new Entry<>(hash, key, value, e); if (size++ >= threshold) //如果大於臨界值就擴容 resize(2 * table.length); //以2的倍數擴容 }
參數bucketIndex就是indexFor函數計算出來的索引值,第2行代碼是取得數組中索引為bucketIndex的Entry對象,第3行就是用hash、key、value構建一個新的Entry對象放到索引為bucketIndex的位置,並且將該位置原先的對象設置為新對象的next構成鏈表。
第4行和第5行就是判斷put后size是否達到了臨界值threshold,如果達到了臨界值就要進行擴容,HashMap擴容是擴為原來的兩倍。
resize()方法如下:
重新調整HashMap的大小,newCapacity是調整后的單位
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里面 table = newTable; //再將newTable賦值給table threshold = (int)(newCapacity * loadFactor);//重新計算臨界值 }
新建了一個HashMap的底層數組,上面代碼中第10行為調用transfer方法,將HashMap的全部元素添加到新的HashMap中,並重新計算元素在新的數組中的索引位置
當HashMap中的元素越來越多的時候,hash沖突的幾率也就越來越高,因為數組的長度是固定的。所以為了提高查詢的效率,就要對HashMap的數組進行擴容,數組擴容這個操作也會出現在ArrayList中,這是一個常用的操作,而在HashMap數組擴容之后,最消耗性能的點就出現了:原數組中的數據必須重新計算其在新數組中的位置,並放進去,這就是resize
那么HashMap什么時候進行擴容呢?當HashMap中的元素個數超過數組大小*loadFactor時,就會進行數組擴容,loadFactor的默認值為0.75,這是一個折中的取值。也就是說,默認情況下,數組大小為16,那么當HashMap中元素個數超過16*0.75=12的時候,就把數組的大小擴展為 2*16=32,即擴大一倍,然后重新計算每個元素在數組中的位置,擴容是需要進行數組復制的,復制數組是非常消耗性能的操作,所以如果我們已經預知HashMap中元素的個數,那么預設元素的個數能夠有效的提高HashMap的性能。
上面講完了put的方法,接下來我們看下get方法:
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算法作為基礎,理解起來這段代碼就很容易了。從上面的源代碼中可以看出:從HashMap中get元素時,首先計算key的hashCode,找到數組中對應位置的某一元素,然后通過key的equals方法在對應位置的鏈表中找到需要的元素。
歸納起來簡單地說,HashMap 在底層將 key-value 當成一個整體進行處理,這個整體就是一個 Entry 對象。HashMap 底層采用一個 Entry[] 數組來保存所有的 key-value 對,當需要存儲一個 Entry 對象時,會根據hash算法來決定其在數組中的存儲位置,在根據equals方法決定其在該數組位置上的鏈表中的存儲位置;當需要取出一個Entry時,也會根據hash算法找到其在數組中的存儲位置,再根據equals方法從該位置上的鏈表中取出該Entry。
四、HashMap重要知識點
1.性能參數:
HashMap 包含如下幾個構造器:
HashMap():構建一個初始容量為 16,負載因子為 0.75 的 HashMap。
HashMap(int initialCapacity):構建一個初始容量為 initialCapacity,負載因子為 0.75 的 HashMap。
HashMap(int initialCapacity, float loadFactor):以指定初始容量、指定的負載因子創建一個 HashMap。
HashMap的基礎構造器HashMap(int initialCapacity, float loadFactor)帶有兩個參數,它們是初始容量initialCapacity和加載因子loadFactor。
initialCapacity:HashMap的最大容量,即為底層數組的長度。
loadFactor:負載因子loadFactor定義為:散列表的實際元素數目(n)/ 散列表的容量(m)。 負載因子衡量的是一個散列表的空間的使用程度,負載因子越大表示散列表的裝填程度越高,反之愈小。對於使用鏈表法的散列表來說,查找一個元素的平均時間是O(1+a),因此如果負載因子越大,對空間的利用更充分,然而后果是查找效率的降低;如果負載因子太小,那么散列表的數據將過於稀疏,對空間造成嚴重浪費。
HashMap的實現中,通過threshold字段來判斷HashMap的最大容量:
threshold = (int)(capacity * loadFactor);
結合負載因子的定義公式可知,threshold就是在此loadFactor和capacity對應下允許的最大元素數目,超過這個數目就重新resize,以降低實際的負載因子。默認的的負載因子0.75是對空間和時間效率的一個平衡選擇。當容量超出此最大容量時, resize后的HashMap容量是容量的兩倍:
if (size++ >= threshold) resize(2 * table.length);
2.HashCode的重要性
HashMap中對Key的HashCode要做一次rehash,防止一些糟糕的Hash算法生成的糟糕的HashCode,那么為什么要防止糟糕的HashCode?
糟糕的HashCode意味着的是Hash沖突,即多個不同的Key可能得到的是同一個HashCode,糟糕的Hash算法意味着的就是Hash沖突的概率增大,這意味着HashMap的性能將下降,表現在兩方面:
1、有10個Key,可能6個Key的HashCode都相同,另外四個Key所在的Entry均勻分布在table的位置上,而某一個位置上卻連接了6個Entry。這就失去了HashMap的意義,HashMap這種數據結構性高性能的前提是,Entry均勻地分布在table位置上,但現在確是1 1 1 1 6的分布。所以,我們要求HashCode有很強的隨機性,這樣就盡可能地可以保證了Entry分布的隨機性,提升了HashMap的效率。
2、HashMap在一個某個table位置上遍歷鏈表的時候的代碼:
if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
看到,由於采用了"&&"運算符,因此先比較HashCode,HashCode都不相同就直接pass了,不會再進行equals比較了。HashCode因為是int值,比較速度非常快,而equals方法往往會對比一系列的內容,速度會慢一些。Hash沖突的概率大,意味着equals比較的次數勢必增多,必然降低了HashMap的效率了。
3.HashMap的table為什么是transient的
一個非常細節的地方:
transient Entry[] table;
看到table用了transient修飾,也就是說table里面的內容全都不會被序列化,這么寫的原因?
因為HashMap是基於HashCode的,HashCode作為Object的方法,是native的:
public native int hashCode();
這意味着的是:HashCode和底層實現相關,不同的虛擬機可能有不同的HashCode算法。再進一步說得明白些就是,可能同一個Key在虛擬機A上的HashCode=1,在虛擬機B上的HashCode=2,在虛擬機C上的HashCode=3。
這就有問題了,Java自誕生以來,就以跨平台性作為最大賣點,好了,如果table不被transient修飾,在虛擬機A上可以用的程序到虛擬機B上可以用的程序就不能用了,失去了跨平台性,因為:
1、Key在虛擬機A上的HashCode=100,連在table[4]上
2、Key在虛擬機B上的HashCode=101,這樣,就去table[5]上找Key,明顯找不到
整個代碼就出問題了。因此,為了避免這一點,Java采取了重寫自己序列化table的方法,在writeObject選擇將key和value追加到序列化的文件最后面:
private void writeObject(java.io.ObjectOutputStream s) throws IOException { Iterator<Map.Entry<K,V>> i = (size > 0) ? entrySet0().iterator() : null; // Write out the threshold, loadfactor, and any hidden stuff s.defaultWriteObject(); // Write out number of buckets s.writeInt(table.length); // Write out size (number of Mappings) s.writeInt(size); // Write out keys and values (alternating) if (i != null) { while (i.hasNext()) { Map.Entry<K,V> e = i.next(); s.writeObject(e.getKey()); s.writeObject(e.getValue()); } } }
而在readObject的時候重構HashMap數據結構:
private void readObject(java.io.ObjectInputStream s) throws IOException, ClassNotFoundException { // Read in the threshold, loadfactor, and any hidden stuff s.defaultReadObject(); // Read in number of buckets and allocate the bucket array; int numBuckets = s.readInt(); table = new Entry[numBuckets]; init(); // Give subclass a chance to do its thing. // Read in size (number of Mappings) int size = s.readInt(); // Read the keys and values, and put the mappings in the HashMap for (int i=0; i<size; i++) { K key = (K) s.readObject(); V value = (V) s.readObject(); putForCreate(key, value); } }
一種麻煩的方式,但卻保證了跨平台性。
這個例子也告訴了我們:盡管使用的虛擬機大多數情況下都是HotSpot,但是也不能對其它虛擬機不管不顧,有跨平台的思想是一件好事。
4.HashMap和Hashtable的區別
HashMap和Hashtable是一組相似的鍵值對集合,它們的區別也是面試常被問的問題之一,我這里簡單總結一下HashMap和Hashtable的區別:
1、Hashtable是線程安全的,Hashtable所有對外提供的方法都使用了synchronized,也就是同步,而HashMap則是線程非安全的
2、Hashtable不允許空的value,空的value將導致空指針異常,而HashMap則無所謂,沒有這方面的限制
3、上面兩個缺點是最主要的區別,另外一個區別無關緊要,我只是提一下,就是兩個的rehash算法不同,Hashtable的是:
private int hash(Object k) { // hashSeed will be zero if alternative hashing is disabled. return hashSeed ^ k.hashCode(); }
這個hashSeed是使用sun.misc.Hashing類的randomHashSeed方法產生的。HashMap的rehash算法上面看過了,也就是:
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); }
五、自己動手寫一個簡單的HashMap
我們來動手實現一個簡單的hashmap
package study.collection; public class MyMap001 { /** * 定義一個數組來存放所有的key 和 Value 的值,這里先假設數組的大小為很大的一個.實際底層為數組加鏈表 數組存在擴容的場景 * 擴容在前面MyArrayList 中講過了,所以這里就不重復去做了,因此設置為了990大小. */ MyEntry[] arr = new MyEntry[990]; /** * 大小 */ int size; public void put(Object key,Object value){ MyEntry e = new MyEntry(key,value); //解決鍵值重復的處理 for(int i=0;i<size;i++){ if(arr[i].key.equals(key)){ arr[i].value=value; return ; } } arr[size++] = e; } public Object get(Object key){ /** * 遍歷尋找 */ for(int i=0;i<size;i++){ if(arr[i].key.equals(key)){ return arr[i].value; } } return null; } /** * 遍歷尋找 * @param key * @return */ public boolean containsKey(Object key){ for(int i=0;i<size;i++){ if(arr[i].key.equals(key)){ return true; } } return false; } public boolean containsValue(Object value){ for(int i=0;i<size;i++){ if(arr[i].value.equals(value)){ return true; } } return false; } public static void main(String[] args) { MyMap001 m = new MyMap001(); m.put("張三", new Wife("楊洋")); m.put("李四", new Wife("王五")); Wife w = (Wife) m.get("張三"); System.out.println(w); } } /** * * Map 中存放的一個key 一個Value * */ class MyEntry { Object key; Object value; public MyEntry(Object key, Object value) { super(); this.key = key; this.value = value; } }
上面的缺陷為:查詢效率低,遍歷元素的過程。因為所有的元素都存在數組中,采用的數組方式,沒有結合鏈表。
改進;結合鏈表機制。
package study.collection; import java.util.LinkedList; public class MyMap002 { LinkedList[] arr = new LinkedList[9]; //Map的底層結構就是:數組+鏈表! 暫時不關注擴容 int size; public void put(Object key,Object value){ MyEntry e = new MyEntry(key,value); //計算hash值 int hash = key.hashCode(); //防止存在負數 hash = hash<0?-hash:hash; //取模 int a = hash%arr.length; if(arr[a]==null) { //此索引處沒有元素 LinkedList list = new LinkedList(); list.add(e); arr[a] = list; }else { //如果存在元素,則先取出 LinkedList list = arr[a]; //遍歷此節點上面的鏈表是否重復元素 for(int i=0;i<list.size();i++) { MyEntry e2 = (MyEntry) list.get(i); //是否重復 if(e2.key.equals(key)){ e2.value = value; //鍵值重復直接覆蓋! return; } } //鏈表節點上面追加元素 arr[a].add(e); } //a:1000-->1 b:10000-->13 } public Object get(Object key){ int a = key.hashCode()%arr.length; if(arr[a]!=null){ LinkedList list = arr[a]; for(int i=0;i<list.size();i++){ MyEntry e = (MyEntry) list.get(i); if(e.key.equals(key)){ return e.value; } } } return null; } public static void main(String[] args) { MyMap002 m = new MyMap002(); m.put("張三", new Wife("楊洋")); m.put("李四", new Wife("王五")); Wife w = (Wife) m.get("張三"); System.out.println(w); } }
上面是一個簡單的實現優化,也只是實現了最基本的功能,但重點在於理解原理和hashmap的原理
六、HashMap源碼注釋
package java.util; import java.io.*; public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable { // 默認的初始容量(容量為HashMap中槽的數目)是16,且實際容量必須是2的整數次冪。 static final int DEFAULT_INITIAL_CAPACITY = 16; // 最大容量(必須是2的冪且小於2的30次方,傳入容量過大將被這個值替換) static final int MAXIMUM_CAPACITY = 1 << 30; // 默認加載因子為0.75 static final float DEFAULT_LOAD_FACTOR = 0.75f; // 存儲數據的Entry數組,長度是2的冪。 // HashMap采用鏈表法解決沖突,每一個Entry本質上是一個單向鏈表 transient Entry[] table; // HashMap的底層數組中已用槽的數量 transient int size; // HashMap的閾值,用於判斷是否需要調整HashMap的容量(threshold = 容量*加載因子) int threshold; // 加載因子實際大小 final float loadFactor; // HashMap被改變的次數 transient volatile int modCount; // 指定“容量大小”和“加載因子”的構造函數 public HashMap(int initialCapacity, float loadFactor) { if (initialCapacity < 0) throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity); // HashMap的最大容量只能是MAXIMUM_CAPACITY if (initialCapacity > MAXIMUM_CAPACITY) initialCapacity = MAXIMUM_CAPACITY; //加載因此不能小於0 if (loadFactor <= 0 || Float.isNaN(loadFactor)) throw new IllegalArgumentException("Illegal load factor: " + loadFactor); // 找出“大於initialCapacity”的最小的2的冪 int capacity = 1; while (capacity < initialCapacity) capacity <<= 1; // 設置“加載因子” this.loadFactor = loadFactor; // 設置“HashMap閾值”,當HashMap中存儲數據的數量達到threshold時,就需要將HashMap的容量加倍。 threshold = (int)(capacity * loadFactor); // 創建Entry數組,用來保存數據 table = new Entry[capacity]; init(); } // 指定“容量大小”的構造函數 public HashMap(int initialCapacity) { this(initialCapacity, DEFAULT_LOAD_FACTOR); } // 默認構造函數。 public HashMap() { // 設置“加載因子”為默認加載因子0.75 this.loadFactor = DEFAULT_LOAD_FACTOR; // 設置“HashMap閾值”,當HashMap中存儲數據的數量達到threshold時,就需要將HashMap的容量加倍。 threshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR); // 創建Entry數組,用來保存數據 table = new Entry[DEFAULT_INITIAL_CAPACITY]; init(); } // 包含“子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); // 將m中的全部元素逐個添加到HashMap中 putAllForCreate(m); } //求hash值的方法,重新計算hash值 static int hash(int h) { h ^= (h >>> 20) ^ (h >>> 12); return h ^ (h >>> 7) ^ (h >>> 4); } // 返回h在數組中的索引值,這里用&代替取模,旨在提升效率 // h & (length-1)保證返回值的小於length static int indexFor(int h, int length) { return h & (length-1); } public int size() { return size; } public boolean isEmpty() { return size == 0; } // 獲取key對應的value public V get(Object key) { if (key == null) return getForNullKey(); // 獲取key的hash值 int hash = hash(key.hashCode()); // 在“該hash值對應的鏈表”上查找“鍵值等於key”的元素 for (Entry<K,V> e = table[indexFor(hash, table.length)]; e != null; e = e.next) { Object k; //判斷key是否相同 if (e.hash == hash && ((k = e.key) == key || key.equals(k))) return e.value; } //沒找到則返回null return null; } // 獲取“key為null”的元素的值 // HashMap將“key為null”的元素存儲在table[0]位置,但不一定是該鏈表的第一個位置! private V getForNullKey() { for (Entry<K,V> e = table[0]; e != null; e = e.next) { if (e.key == null) return e.value; } return null; } // HashMap是否包含key public boolean containsKey(Object key) { return getEntry(key) != null; } // 返回“鍵為key”的鍵值對 final Entry<K,V> getEntry(Object key) { // 獲取哈希值 // HashMap將“key為null”的元素存儲在table[0]位置,“key不為null”的則調用hash()計算哈希值 int hash = (key == null) ? 0 : hash(key.hashCode()); // 在“該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-value”添加到HashMap中 public V put(K key, V value) { // 若“key為null”,則將該鍵值對添加到table[0]中。 if (key == null) return putForNullKey(value); // 若“key不為null”,則計算該key的哈希值,然后將其添加到該哈希值對應的鏈表中。 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; // 若“該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; } } // 若“該key”對應的鍵值對不存在,則將“key-value”添加到table中 modCount++; //將key-value添加到table[i]處 addEntry(hash, key, value, i); return null; } // putForNullKey()的作用是將“key為null”鍵值對添加到table[0]位置 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; } } // 如果沒有存在key為null的鍵值對,則直接題阿見到table[0]處! modCount++; addEntry(0, null, value, 0); return null; } // 創建HashMap對應的“添加方法”, // 它和put()不同。putForCreate()是內部方法,它被構造函數等調用,用來創建HashMap // 而put()是對外提供的往HashMap中添加元素的方法。 private void putForCreate(K key, V value) { int hash = (key == null) ? 0 : hash(key.hashCode()); int i = indexFor(hash, table.length); // 若該HashMap表中存在“鍵值等於key”的元素,則替換該元素的value值 for (Entry<K,V> e = table[i]; e != null; e = e.next) { Object k; if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) { e.value = value; return; } } // 若該HashMap表中不存在“鍵值等於key”的元素,則將該key-value添加到HashMap中 createEntry(hash, key, value, i); } // 將“m”中的全部元素都添加到HashMap中。 // 該方法被內部的構造HashMap的方法所調用。 private void putAllForCreate(Map<? extends K, ? extends V> m) { // 利用迭代器將元素逐個添加到HashMap中 for (Iterator<? extends Map.Entry<? extends K, ? extends V>> i = m.entrySet().iterator(); i.hasNext(); ) { Map.Entry<? extends K, ? extends V> e = i.next(); putForCreate(e.getKey(), e.getValue()); } } // 重新調整HashMap的大小,newCapacity是調整后的容量 void resize(int newCapacity) { Entry[] oldTable = table; int oldCapacity = oldTable.length; //如果就容量已經達到了最大值,則不能再擴容,直接返回 if (oldCapacity == MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return; } // 新建一個HashMap,將“舊HashMap”的全部元素添加到“新HashMap”中, // 然后,將“新HashMap”賦值給“舊HashMap”。 Entry[] newTable = new Entry[newCapacity]; transfer(newTable); table = newTable; threshold = (int)(newCapacity * loadFactor); } // 將HashMap中的全部元素都添加到newTable中 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); } } } // 將"m"的全部元素都添加到HashMap中 public void putAll(Map<? extends K, ? extends V> m) { // 有效性判斷 int numKeysToBeAdded = m.size(); if (numKeysToBeAdded == 0) return; // 計算容量是否足夠, // 若“當前閥值容量 < 需要的容量”,則將容量x2。 if (numKeysToBeAdded > threshold) { int targetCapacity = (int)(numKeysToBeAdded / loadFactor + 1); if (targetCapacity > MAXIMUM_CAPACITY) targetCapacity = MAXIMUM_CAPACITY; int newCapacity = table.length; while (newCapacity < targetCapacity) newCapacity <<= 1; if (newCapacity > table.length) resize(newCapacity); } // 通過迭代器,將“m”中的元素逐個添加到HashMap中。 for (Iterator<? extends Map.Entry<? extends K, ? extends V>> i = m.entrySet().iterator(); i.hasNext(); ) { Map.Entry<? extends K, ? extends V> e = i.next(); put(e.getKey(), e.getValue()); } } // 刪除“鍵為key”元素 public V remove(Object key) { Entry<K,V> e = removeEntryForKey(key); return (e == null ? null : e.value); } // 刪除“鍵為key”的元素 final Entry<K,V> removeEntryForKey(Object key) { // 獲取哈希值。若key為null,則哈希值為0;否則調用hash()進行計算 int hash = (key == null) ? 0 : hash(key.hashCode()); int i = indexFor(hash, table.length); Entry<K,V> prev = table[i]; Entry<K,V> e = prev; // 刪除鏈表中“鍵為key”的元素 // 本質是“刪除單向鏈表中的節點” while (e != null) { Entry<K,V> next = e.next; Object k; if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) { modCount++; size--; if (prev == e) table[i] = next; else prev.next = next; e.recordRemoval(this); return e; } prev = e; e = next; } return e; } // 刪除“鍵值對” final Entry<K,V> removeMapping(Object o) { if (!(o instanceof Map.Entry)) return null; Map.Entry<K,V> entry = (Map.Entry<K,V>) o; Object key = entry.getKey(); int hash = (key == null) ? 0 : hash(key.hashCode()); int i = indexFor(hash, table.length); Entry<K,V> prev = table[i]; Entry<K,V> e = prev; // 刪除鏈表中的“鍵值對e” // 本質是“刪除單向鏈表中的節點” while (e != null) { Entry<K,V> next = e.next; if (e.hash == hash && e.equals(entry)) { modCount++; size--; if (prev == e) table[i] = next; else prev.next = next; e.recordRemoval(this); return e; } prev = e; e = next; } return e; } // 清空HashMap,將所有的元素設為null public void clear() { modCount++; Entry[] tab = table; for (int i = 0; i < tab.length; i++) tab[i] = null; size = 0; } // 是否包含“值為value”的元素 public boolean containsValue(Object value) { // 若“value為null”,則調用containsNullValue()查找 if (value == null) return containsNullValue(); // 若“value不為null”,則查找HashMap中是否有值為value的節點。 Entry[] tab = table; for (int i = 0; i < tab.length ; i++) for (Entry e = tab[i] ; e != null ; e = e.next) if (value.equals(e.value)) return true; return false; } // 是否包含null值 private boolean containsNullValue() { Entry[] tab = table; for (int i = 0; i < tab.length ; i++) for (Entry e = tab[i] ; e != null ; e = e.next) if (e.value == null) return true; return false; } // 克隆一個HashMap,並返回Object對象 public Object clone() { HashMap<K,V> result = null; try { result = (HashMap<K,V>)super.clone(); } catch (CloneNotSupportedException e) { // assert false; } result.table = new Entry[table.length]; result.entrySet = null; result.modCount = 0; result.size = 0; result.init(); // 調用putAllForCreate()將全部元素添加到HashMap中 result.putAllForCreate(this); return result; } // Entry是單向鏈表。 // 它是 “HashMap鏈式存儲法”對應的鏈表。 // 它實現了Map.Entry 接口,即實現getKey(), getValue(), setValue(V value), equals(Object o), hashCode()這些函數 static class Entry<K,V> implements Map.Entry<K,V> { final K key; V value; // 指向下一個節點 Entry<K,V> next; final int hash; // 構造函數。 // 輸入參數包括"哈希值(h)", "鍵(k)", "值(v)", "下一節點(n)" Entry(int h, K k, V v, Entry<K,V> n) { value = v; next = n; key = k; hash = h; } public final K getKey() { return key; } public final V getValue() { return value; } public final V setValue(V newValue) { V oldValue = value; value = newValue; return oldValue; } // 判斷兩個Entry是否相等 // 若兩個Entry的“key”和“value”都相等,則返回true。 // 否則,返回false public final boolean equals(Object o) { if (!(o instanceof Map.Entry)) return false; Map.Entry e = (Map.Entry)o; Object k1 = getKey(); Object k2 = e.getKey(); if (k1 == k2 || (k1 != null && k1.equals(k2))) { Object v1 = getValue(); Object v2 = e.getValue(); if (v1 == v2 || (v1 != null && v1.equals(v2))) return true; } return false; } // 實現hashCode() public final int hashCode() { return (key==null ? 0 : key.hashCode()) ^ (value==null ? 0 : value.hashCode()); } public final String toString() { return getKey() + "=" + getValue(); } // 當向HashMap中添加元素時,繪調用recordAccess()。 // 這里不做任何處理 void recordAccess(HashMap<K,V> m) { } // 當從HashMap中刪除元素時,繪調用recordRemoval()。 // 這里不做任何處理 void recordRemoval(HashMap<K,V> m) { } } // 新增Entry。將“key-value”插入指定位置,bucketIndex是位置索引。 void addEntry(int hash, K key, V value, int bucketIndex) { // 保存“bucketIndex”位置的值到“e”中 Entry<K,V> e = table[bucketIndex]; // 設置“bucketIndex”位置的元素為“新Entry”, // 設置“e”為“新Entry的下一個節點” table[bucketIndex] = new Entry<K,V>(hash, key, value, e); // 若HashMap的實際大小 不小於 “閾值”,則調整HashMap的大小 if (size++ >= threshold) resize(2 * table.length); } // 創建Entry。將“key-value”插入指定位置。 void createEntry(int hash, K key, V value, int bucketIndex) { // 保存“bucketIndex”位置的值到“e”中 Entry<K,V> e = table[bucketIndex]; // 設置“bucketIndex”位置的元素為“新Entry”, // 設置“e”為“新Entry的下一個節點” table[bucketIndex] = new Entry<K,V>(hash, key, value, e); size++; } // HashIterator是HashMap迭代器的抽象出來的父類,實現了公共了函數。 // 它包含“key迭代器(KeyIterator)”、“Value迭代器(ValueIterator)”和“Entry迭代器(EntryIterator)”3個子類。 private abstract class HashIterator<E> implements Iterator<E> { // 下一個元素 Entry<K,V> next; // expectedModCount用於實現fast-fail機制。 int expectedModCount; // 當前索引 int index; // 當前元素 Entry<K,V> current; HashIterator() { expectedModCount = modCount; if (size > 0) { // advance to first entry Entry[] t = table; // 將next指向table中第一個不為null的元素。 // 這里利用了index的初始值為0,從0開始依次向后遍歷,直到找到不為null的元素就退出循環。 while (index < t.length && (next = t[index++]) == null) ; } } public final boolean hasNext() { return next != null; } // 獲取下一個元素 final Entry<K,V> nextEntry() { if (modCount != expectedModCount) throw new ConcurrentModificationException(); Entry<K,V> e = next; if (e == null) throw new NoSuchElementException(); // 注意!!! // 一個Entry就是一個單向鏈表 // 若該Entry的下一個節點不為空,就將next指向下一個節點; // 否則,將next指向下一個鏈表(也是下一個Entry)的不為null的節點。 if ((next = e.next) == null) { Entry[] t = table; while (index < t.length && (next = t[index++]) == null) ; } current = e; return e; } // 刪除當前元素 public void remove() { if (current == null) throw new IllegalStateException(); if (modCount != expectedModCount) throw new ConcurrentModificationException(); Object k = current.key; current = null; HashMap.this.removeEntryForKey(k); expectedModCount = modCount; } } // value的迭代器 private final class ValueIterator extends HashIterator<V> { public V next() { return nextEntry().value; } } // key的迭代器 private final class KeyIterator extends HashIterator<K> { public K next() { return nextEntry().getKey(); } } // Entry的迭代器 private final class EntryIterator extends HashIterator<Map.Entry<K,V>> { public Map.Entry<K,V> next() { return nextEntry(); } } // 返回一個“key迭代器” Iterator<K> newKeyIterator() { return new KeyIterator(); } // 返回一個“value迭代器” Iterator<V> newValueIterator() { return new ValueIterator(); } // 返回一個“entry迭代器” Iterator<Map.Entry<K,V>> newEntryIterator() { return new EntryIterator(); } // HashMap的Entry對應的集合 private transient Set<Map.Entry<K,V>> entrySet = null; // 返回“key的集合”,實際上返回一個“KeySet對象” public Set<K> keySet() { Set<K> ks = keySet; return (ks != null ? ks : (keySet = new KeySet())); } // Key對應的集合 // KeySet繼承於AbstractSet,說明該集合中沒有重復的Key。 private final class KeySet extends AbstractSet<K> { public Iterator<K> iterator() { return newKeyIterator(); } public int size() { return size; } public boolean contains(Object o) { return containsKey(o); } public boolean remove(Object o) { return HashMap.this.removeEntryForKey(o) != null; } public void clear() { HashMap.this.clear(); } } // 返回“value集合”,實際上返回的是一個Values對象 public Collection<V> values() { Collection<V> vs = values; return (vs != null ? vs : (values = new Values())); } // “value集合” // Values繼承於AbstractCollection,不同於“KeySet繼承於AbstractSet”, // Values中的元素能夠重復。因為不同的key可以指向相同的value。 private final class Values extends AbstractCollection<V> { public Iterator<V> iterator() { return newValueIterator(); } public int size() { return size; } public boolean contains(Object o) { return containsValue(o); } public void clear() { HashMap.this.clear(); } } // 返回“HashMap的Entry集合” public Set<Map.Entry<K,V>> entrySet() { return entrySet0(); } // 返回“HashMap的Entry集合”,它實際是返回一個EntrySet對象 private Set<Map.Entry<K,V>> entrySet0() { Set<Map.Entry<K,V>> es = entrySet; return es != null ? es : (entrySet = new EntrySet()); } // EntrySet對應的集合 // EntrySet繼承於AbstractSet,說明該集合中沒有重復的EntrySet。 private final class EntrySet extends AbstractSet<Map.Entry<K,V>> { public Iterator<Map.Entry<K,V>> iterator() { return newEntryIterator(); } public boolean contains(Object o) { if (!(o instanceof Map.Entry)) return false; Map.Entry<K,V> e = (Map.Entry<K,V>) o; Entry<K,V> candidate = getEntry(e.getKey()); return candidate != null && candidate.equals(e); } public boolean remove(Object o) { return removeMapping(o) != null; } public int size() { return size; } public void clear() { HashMap.this.clear(); } } // java.io.Serializable的寫入函數 // 將HashMap的“總的容量,實際容量,所有的Entry”都寫入到輸出流中 private void writeObject(java.io.ObjectOutputStream s) throws IOException { Iterator<Map.Entry<K,V>> i = (size > 0) ? entrySet0().iterator() : null; // Write out the threshold, loadfactor, and any hidden stuff s.defaultWriteObject(); // Write out number of buckets s.writeInt(table.length); // Write out size (number of Mappings) s.writeInt(size); // Write out keys and values (alternating) if (i != null) { while (i.hasNext()) { Map.Entry<K,V> e = i.next(); s.writeObject(e.getKey()); s.writeObject(e.getValue()); } } } private static final long serialVersionUID = 362498820763181265L; // java.io.Serializable的讀取函數:根據寫入方式讀出 // 將HashMap的“總的容量,實際容量,所有的Entry”依次讀出 private void readObject(java.io.ObjectInputStream s) throws IOException, ClassNotFoundException { // Read in the threshold, loadfactor, and any hidden stuff s.defaultReadObject(); // Read in number of buckets and allocate the bucket array; int numBuckets = s.readInt(); table = new Entry[numBuckets]; init(); // Give subclass a chance to do its thing. // Read in size (number of Mappings) int size = s.readInt(); // Read the keys and values, and put the mappings in the HashMap for (int i=0; i<size; i++) { K key = (K) s.readObject(); V value = (V) s.readObject(); putForCreate(key, value); } } // 返回“HashMap總的容量” int capacity() { return table.length; } // 返回“HashMap的加載因子” float loadFactor() { return loadFactor; } }
參考資料:
http://www.cnblogs.com/ITtangtang/p/3948406.html
http://blog.csdn.net/ns_code/article/details/36034955
http://www.cnblogs.com/xrq730/p/5030920.html
http://blog.csdn.net/chenssy