當前位置:編程學習大全網 - 源碼下載 - Hashmap原始源代碼

Hashmap原始源代碼

HashMap和HashSet是Java集合框架的兩個重要成員,其中HashMap是Map接口的通用實現類,HashSet是Set接口的通用實現類。雖然HashMap和HashSet實現的接口規範不同,但是它們底層的哈希存儲機制是完全壹樣的,甚至HashSet本身也是由HashMap實現的。

通過HashMap和HashSet的源代碼分析了Hash存儲機制。

事實上,HashSet和HashMap有很多相似之處。對於HashSet,系統使用哈希算法來確定集合元素的存儲位置,可以保證集合元素能夠快速存儲和檢索。對於HashMap,將系統的鍵值作為壹個整體來對待,鍵值的存儲位置總是根據Hash算法來計算,這樣可以保證映射的鍵值對能夠被快速的存儲和檢索。

在介紹集合存儲之前,需要指出的是,雖然集合聲稱存儲Java對象,但實際上並沒有將Java對象放入集合集合中,只是在集合集合中保留對這些對象的引用。換句話說,Java集合實際上是指向實際Java對象的多個引用變量的集合。

收藏和參考

就像引用類型的數組,當我們把Java對象放入數組時,並不是真的把Java對象放入數組,只是把對對象的引用放入數組,每個數組元素都是壹個引用變量。

HashMap的存儲實現

當壹個程序試圖將多個鍵值放入壹個HashMap時,以下面的代碼片段為例:

Java代碼

HashMap & lt字符串,Double & gtmap = new HashMap & lt字符串,Double & gt();

Map.put("中文",80.0);

Map.put(《數學》,89.0);

Map.put("英語",78.2);

HashMap使用壹種所謂的“哈希算法”來確定每個元素的存儲位置。

當程序執行map.put("中文",80.0);,系統會調用“中文”的hashCode()方法來獲取它的hashCode值——每個Java對象都有壹個hashCode()方法,可以用來獲取它的hashCode值。得到這個對象的hashCode值後,系統會根據hashCode值確定元素的存儲位置。

我們可以看看HashMap類的put(K key,V value)方法的源代碼:

Java代碼

公共V put(K鍵,V值)

{

//如果key為null,則調用putForNullKey方法進行處理。

if (key == null)

返回putForNullKey(值);

//根據key的鍵碼計算哈希值。

int hash = hash(key . hashcode());

//在相應的表中搜索指定哈希值的索引。

int i = indexFor(hash,table . length);

//如果I索引處的條目不為空,則通過循環連續遍歷E元素的下壹個元素。

for(Entry & lt;k,V & gte =表[I];e!= nulle = e.next)

{

對象k;

//發現指定的鍵等於要放入的鍵(哈希值相同

//通過等於比較返回true)

if(e . hash = = hash & amp;& amp((k = e.key) == key

|| key.equals(k)))

{

V oldValue = e.value

e.value =值;

e . record access(this);

返回舊值;

}

}

//如果I索引處的條目為空,則此處沒有條目。

modcount++;

//將鍵和值添加到I索引。

addEntry(hash,key,value,I);

返回null

}

上面的程序中使用了壹個重要的內部接口:Map。條目和每張地圖。Entry實際上是壹個鍵值對。從上面的程序可以看出,當系統決定在HashMap中存儲鍵-值對時,完全不考慮條目中的值,只根據鍵計算確定每個條目的存儲位置。這也解釋了前面的結論:我們完全可以把映射集中的值看作是鍵的附件,當系統確定了鍵的存儲位置,就可以把值保存在那裏。

上述方法提供了壹種根據hashcode (): hash()的返回值計算HashCode的方法,這是壹種純數學計算,其方法如下:

Java代碼

靜態整數哈希(整數h)

{

^=(h & gt;& gt& gt^(h & gt;& gt& gt12);

返回h ^(h & gt;& gt& gt7)^(h & gt;& gt& gt4);

}

對於任何給定的對象,只要它的HashCode()返回值相同,那麽通過調用hash(int h)方法計算出來的哈希代碼值總是相同的。接下來,程序將調用indexFor(int h,int length)方法來計算對象應該存儲在表數組中的哪個索引。indexFor(int h,int length)方法的代碼如下:

Java代碼

靜態整數索引For(整數h,整數長度)

{

返回h & amp(長度-1);

}

這個方法很巧妙,它總是通過H &;(table.length -1)來獲取對象的存儲位置——而HashMap底部數組的長度永遠是2的n次方,這個可以在後面HashMap構造函數的介紹中看到。

當長度總是2的倍數時,H &;(length-1)會是壹個非常巧妙的設計:假設H = 5,Length = 16,那麽h & Length-1會得到5;如果h = 6,長度= 16,那麽h &;長度-1將得到6...如果H = 15,長度= 16,則H &;長度-1會得到15;但當h=16,長度=16時,則h &;長度-1會得到0;當h=17,長度=16時,則h &;長度-1將得到1...這確保了計算出的索引值總是在表數組的索引內。

根據上面put方法的源代碼可以看出,當程序試圖將壹個鍵-值對放入壹個HashMap中時,程序首先根據鍵的hashCode()返回值確定條目的存儲位置:如果兩個條目的鍵的hashCode()返回值相同,那麽它們的存儲位置相同。如果這兩個條目的鍵通過相等比較返回true,則新添加的條目的值將覆蓋集合中原始條目的值,但鍵不會覆蓋它。如果這兩個條目的鍵通過相等比較返回false,那麽新添加的條目將與集合中的原條目形成壹個條目鏈,新添加的條目位於條目鏈的頭部——詳見addEntry()方法的描述。

當壹個鍵值對被添加到HashMap中時,鍵值對(即Entry對象)的存儲位置由其key hashCode()的返回值決定。當兩個Entry對象的key的hashCode()返回值相同時,key會通過eqauls()比較值,決定是采用覆蓋行為(返回true)還是生成壹個Entry鏈(返回false)。

上面的程序中還調用了AddEntry(hash,key,value,I);代碼,其中addEntry是HashMap提供的包訪問方法,只用於添加壹個鍵值對。下面是這個方法的代碼:

Java代碼

void addEntry(int hash,K key,V value,int bucketIndex)

{

//獲取指定bucketIndex索引處的條目。

Entry & ltk,V & gte =表[bucket index];// ①

//將新創建的條目放入bucketIndex,並使新條目指向原始條目。

table[bucketIndex] =新條目& ltk,V & gt(hash,key,value,e);

//如果映射中的鍵-值對的數量超過限制,

if(size++ & gt;=閾值)

//將表格對象的長度擴展到2倍。

調整大小(2 * table . length);// ②

}

上面方法的代碼非常簡單,但是包含了壹個非常優雅的設計:系統總是將新添加的Entry對象放入表數組的bucketIndex中——如果bucketIndex處已經有壹個Entry對象,那麽新添加的Entry對象指向原來的Entry對象(生成壹個Entry鏈)。如果bucketIndex索引處沒有Entry對象,即上面程序中代碼1的e變量為null,即新添加的Entry對象指向null,即沒有生成Entry鏈。

JDK源代碼

您可以在JDK安裝目錄中找到壹個src.zip壓縮文件,其中包含Java基本類庫的所有源文件。只要讀者有興趣學習,隨時可以打開這個壓縮文件閱讀Java類庫的源代碼,對提高讀者的編程能力很有幫助。需要指出的是,src.zip中包含的源代碼並不像上面那樣包含中文註釋,這些註釋都是作者自己添加的。

哈希算法的性能選項

根據上面的代碼可以看出,在同壹個桶存儲入口鏈的情況下,新放入的入口總是位於桶中,最先放入桶中的入口位於這個入口鏈的末端。

上述程序中有兩個變量:

* size:這個變量保存這個HashMap中包含的鍵值對的數量。

* threshold:這個變量包含了HashMap所能容納的鍵值對的限制,它的值等於HashMap的容量乘以負載因子。

從上面程序中的代碼②可以看出,當size++大於;= threshold,HashMap會自動調用resize方法來擴展HashMap的容量。每次擴展,HashMap的容量都會翻倍。

上面程序中使用的表實際上是壹個普通的數組,每個數組都有固定的長度,這個數組的長度就是HashMap的容量。HashMap包含以下構造函數:

* HashMap():構建壹個HashMap,初始容量為16,加載因子為0.75。

* HashMap(int initialCapacity):用initialCapacity和load factor 0.75構建壹個HashMap。

* HashMap (int initial capacity,float load factor):用指定的初始容量和指定的加載因子創建壹個HashMap。

當創建壹個散列表時,系統會自動創建壹個表數組來保存散列表中的條目。下面是HashMap中構造函數的代碼:

Java代碼

//創建壹個具有指定初始化容量和加載因子的HashMap。

public HashMap(int initial capacity,float loadFactor)

{

//初始容量不能為負。

if(initial capacity & lt;0)

拋出新的IllegalArgumentException(

"非法初始容量:"+

初始容量);

//如果初始容量大於最大容量,我來顯示容量。

if(initial capacity & gt;最大容量)

initial CAPACITY = max _ CAPACITY;

//加載因子必須大於0。

if(負載系數& lt= 0 || Float.isNaN(loadFactor))

拋出新的IllegalArgumentException(

負載系數);

//計算大於initialCapacity的最小2次方值。

int容量= 1;

while(容量& lt初始容量)

容量& lt& lt= 1;

this.loadFactor =負載系數;

//將容量限制設置為容量*負載系數。

閾值= (int)(容量*負載系數);

//初始化表格數組

table =新條目[容量];// ①

init();

}

上面代碼中的加粗代碼包含了壹個簡潔的代碼實現:求大於initialCapacity的2的n次方的最小值,並將其作為HashMap的實際容量(由Capacity變量保存)。例如,給定初始容量10,HashMap的實際容量是16。

從程序①的代碼中可以看出,table的本質是壹個數組,壹個長度為容量的數組。

對於HashMap及其子類,他們使用哈希算法來確定元素在集合中的存儲位置。當系統開始初始化HashMap時,系統會創建壹個長度為capacity的入口數組。這個數組中可以存放元素的位置稱為“桶”,每個桶都有其指定的索引,所以系統可以根據其索引快速訪問桶中存放的元素。

在任何時候,HashMap的每個桶只存儲壹個元素(即壹個條目)。因為Entry對象可以包含壹個引用變量(即Entry構造函數的最後壹個參數)來指向下壹個條目,所以可能會出現HashMap的桶中只有壹個條目,但是這個條目指向了另壹個條目——這就形成了壹個條目鏈。如圖1所示:

圖1。HashMap的存儲圖

讀取HashMap的實現

當HashMap的每個桶中存儲的條目只有壹個條目時——也就是說,條目鏈不是通過指針生成的——HashMap的性能最好:當程序通過鍵取相應的值時,系統只需要先計算鍵的返回值。根據hashCode的返回值找出表數組中鍵的索引,然後取出索引處的條目,最後返回鍵對應的值。看看HashMap類的get(K key)方法代碼:

Java代碼

public V get(對象鍵)

{

//如果鍵為空,則調用getForNullKey獲取相應的值。

if (key == null)

返回getForNullKey();

//根據關鍵字的hashcode值計算關鍵字的hashCode。

int hash = hash(key . hashcode());

//直接取表數組中指定索引處的值,

for(Entry & lt;k,V & gte = table[indexFor(hash,table . length)];

e!= null

//搜索入口鏈的下壹個入口。

e = e.next) // ①

{

對象k;

//如果條目的關鍵字與搜索到的關鍵字相同。

if(e . hash = = hash & amp;& amp((k = e.key) == key

|| key.equals(k)))

返回e.value

}

返回null

}

從上面的代碼可以看出,如果HashMap的每個桶中只有壹個條目,HashMap可以根據索引快速檢索桶中的條目;在“哈希沖突”的情況下,單個桶存儲的不是壹個條目,而是壹個條目鏈,系統只能依次遍歷每個條目,直到找到想要搜索的條目——如果想要搜索的條目位於條目鏈的末端(最早放入桶中),系統必須循環到末端才能找到元素。

綜上所述,HashMap在底層把key-value當做壹個整體,這個整體就是壹個Entry對象。HashMap的底層使用Entry[]數組來存儲所有的鍵值對。當壹個入口對象需要存儲時,它的存儲位置將根據哈希算法來確定。當需要取出壹個條目時,會根據哈希算法找到它的存儲位置,直接取出該條目。可以看出,HashMap之所以能夠快速存儲和檢索它所包含的條目,和我媽從小在現實生活中教給我們的完全類似:不同的東西要放在不同的位置,需要的時候可以快速找到。

創建HashMap時,有壹個默認的加載因子(默認值為0.75),這是時間和空間開銷的折中:增加加載因子可以減少哈希表(即條目數組)占用的內存空間,但會增加查詢數據的時間開銷,而查詢是最頻繁的操作(HashMap的get()和put()方法都使用查詢);降低加載因子會提高數據查詢的性能,但會增加哈希表占用的內存空間。

掌握以上知識後,我們可以在創建HashMap時根據實際需要適當調整load factor的值;如果程序在意空間成本,內存緊張,可以適當增加加載因子;如果程序關心時間開銷,內存充裕,可以適當降低加載因子。通常,程序員不需要改變加載因子的值。

如果壹開始就知道HashMap會保存多個鍵值對,那麽在創建它的時候可以使用更大的初始化容量。如果HashMap中的條目數從未超過capacity * load factor,HashMap就不需要調用resize()方法來重新分配表數組,從而保證更好的性能。當然,壹開始就把初始容量設置的太高可能會浪費空間(系統需要創建壹個容量長度的入口數組),所以在創建HashMap的時候也要謹慎對待初始容量的設置。

  • 上一篇:如何看待Java綠色線程的相關應用效果
  • 下一篇:Ssm財務項目源代碼
  • copyright 2024編程學習大全網