hashTable繼承自dic類,同時實作了map接口和Cloneable、Serializable兩個接口,代表該類是可複制、序列化的類。
ps:dic類和map類較為相似,是一個抽象的hash映射類,包含了一些簡單的空方法和接口。
private transient Entry<?,?>[] table;
瞬時數組變量,它就是hashtable中,最核心的資料存儲區域。
數組長度,不知道大家發現沒有,jdk非常喜歡用一個獨立變量來表示容器中資料的大小,而不是每次傳回核心資料的size或length。
門檻值,這個之前專門強調過,這裡簡單說下,他是容積和負載因子的乘積,表示的含義是目前容器中,能表現出較好性能的資料量上限。超過這個上限時,容器的性能将會有比較大的下降。注意容積和門檻值是有差別的。
threshold ['θrhold] n. 入口;門檻;開始;極限;臨界值
負載因子,是用來設定目前容器中,元素的填充率的。
你可以了解成容器是一個城市,這個城市中最佳入住率的一個上限是負載因子。這個城市的入住使用者最佳的數目,就是他的門檻值。
接下來是modCount ,這個變量的意義是,記錄hashtable中,被修改的次數(包括增、删、改)三個操作的。而其用途呢,是未來被用作判定快速失敗時(fail-fast)的依據資料。關于快速失敗,這個我會在下邊講到。大家這裡隻要知道modCount這個變量的表示的含義是什麼就可以。
然後是版本序列号
接着是構造函數,參數分别為初始容積和負載因子。
函數内會首先判斷初始容積和負載因子是否為正數。
接着如果初始容積為0,則賦予預設值1.也就是說,真實的容積至少都要為1。
接着對table賦予初始值,一個長度為初始容積大小的Entry數組。(防盜連接配接:本文首發自http://www.cnblogs.com/jilodream/ )接着計算
門檻值=(初始容積和負載因子的乘積),(目前系統中最大的數組長度+1),二者的最小值。
也就是說門檻值不能超過數組的最大長度+1。這裡注意一個isNaN()方法,是個很有意思的方法,研究該方法的源碼後,你會覺得很有意思。這個我會在以後的文章中講到。
隻要初始容積的構造函數,負載因子預設為0.75
無參的構造函數,初始容積使用為11,負載因子為0.75
不知道大家發現沒,盡管提供了一個可能的,但是jdk的源碼往往系統提供多個,應用于不同場景的接口,這些接口往往其實隻是對自身其他接口的一個适配。但是對于調用者來說,這樣卻很舒服。
接着是最後一個構造函數,參數為一個map,map的k,v分别繼承自hashTable中的K,V.
函數首先調用一遍通用的構造函數,負載因子為0.75。初始容積為map長度的兩倍以及預設的11,二者的較大值。也就是說對于初始容積來說,最小都要取到11。
接着調用putAll方法,将map中的資料添加到HashTable中。
size()方法,方法采用同步機制,傳回count變量。由于容器中并不是所有的元素都占滿了資料,是以直接用變量傳回值的速度和效率會更高點。同時由于count會随時變動,這裡采用同步方法的形式進行線程保護。
isEmpyt,判斷目前數組是否為空,與size()方法一緻。
keys,elements方法,分别傳回傳回hashTable中所有的key和value的枚舉集合。
這裡KEYS,VALUES為靜态int常量。getEnumeration在下文中會提到。另外與前邊的方法相同,這裡也是對整個方法進行同步加鎖。
接着是contains方法,方法意義不再贅述。
實作邏輯,首先判斷value是否為null,如果為null則直接抛出空引用。
接着将table變量指派給tab臨時變量。然後循環tab,依次取出tab中的entry,以及entry的後繼元素。(防盜連接配接:本文首發自http://www.cnblogs.com/jilodream/ )如果元素的value equals()判斷等于參數value,則直接傳回true。整個方法結束後,為發現,則會傳回false。同時方法本身也是加同步鎖進行線程安全保護。
接着是實作map接口的抽象方法,隻是對contains方法進行了一層封裝。
接着是線程同步方法:containsKey,方法含義不贅述,邏輯如下:
設定臨時變量并指派table。取出key的hashCode。注意這裡并沒有判定key是否為null。
而前文中的value則是判定的。這是由于value是作為equals方法的參數的。即使是null也無法被發現,但是判定一個映射的value為null表示的真的為null還是沒有映射到,這很歧義,是以幹脆直接抛出異常。回到正文,根據hashCode計算出其在table數組中的索引。其實就是取低8位數字然後除以數組length取餘數。(防盜連接配接:本文首發自http://www.cnblogs.com/jilodream/ )
接着依次循環table該索引的後繼元素,判定是否equals()相等。如果有則傳回true。如果始終沒有找到,則傳回false。
get()方法,與containsKey方法的邏輯是一緻的。不同點是,在傳回結果是,如果确實存在該key,則傳回對應的value,否則傳回null。
接着是前文提到的數組最大數字常亮。這裡注意看參數的注釋。部分虛拟機是設定數組的長度限制的。如果超出,可能會導緻OOM異常
接着是rehash方法。這個方法是一個受保護方法。會在接下來的,hashtable添加元素的場景中被調用。他的作用呢,就是重新申請一塊大小合适的記憶體。然後将鍵值元素重新安置到這塊元素中。
那麼就需要兩個步驟。
1、計算新記憶體的大小。
2、計算元素在新table中的位置。
先看代碼:
代碼中會首先擷取舊table的長度oldCapacity 。然後oldCapacity 乘以2再加1.算出新table的長度newCapacity 。
接着判斷newCapacity 是否超出了hashtable所能設定的最大值:MAX_ARRAY_SIZE。如果超出,則判斷oldCapacity 是否已經等于最大值。如果已經等于,則認定,目前hashtable的長度已經到達所允許的上限。無法再繼續擴容。則直接傳回。
否則将MAX_ARRAY_SIZE指派給newCapacity 。作為新的長度。也就是說rehash在大小允許的情況下,一般會翻倍擴容。但是如果翻倍後長度超出上限,則以上限大小作為擴容後新的大小。
接着以newCapacity 作為長度,new出一個Entry數組,作為新的table元素存放容器。
modCount自加1。
接着計算門檻值:newCapacity 乘以負載因子和MAX_ARRAY_SIZE+1 取較小值。注意這裡負載因子是可以大于1的。是以newCapacity 乘以負載因子,式可以大于MAX_ARRAY_SIZE的。
接着就是計算舊有table中的鍵值元素在新table中的位置了:這裡使用的是雙層循環,外層依次周遊Entry主數組上的元素。如果entry[i]不等于null值,則将該元素及其後繼元素依次計算出新的位置,然後插入到主數組上的對應位置。同時将主數組中原來位置的元素。作為新放置元素的後繼。也就是每個新元素,插在每個對應位置的連結清單最前側。至于為什麼不放在這個對應連結清單的最後位置。其實很簡單,因為這是一個鍊式存儲結構,需要依次周遊每個元素,才能找到隊尾的元素。
接着是添加元素的私有方法addEntry。(防盜連接配接:本文首發自http://www.cnblogs.com/jilodream/ )
首先是modCount自加1.
接着如果目前table的數量已經超過了門檻值,那麼就進行一次rehash,接着根據key的hashCode計算出目前鍵值對的輸入索引。接着取出table對應索引位置的元素一同做出一個新的Entry元素放在這個對應索引的位置上。(這裡要注意後續的entry 的構造方法)同時count數目自加1。這裡需要注意的是,目前數目如果已經超過門檻值,前邊講到的rehash是不一定會重新做出新數組的(length超過了MAX_ARRAY_SIZE的限制時)很多人在了解這裡的時候,就認定隻要count超過門檻值就一定會重新配置設定table記憶體的位址,這個了解是存在問題的。
接着是put方法。這個方法是hashtable非常常用的一個public方法。方法本身是一個同方法。在方法中對于參數value和key有邏輯:如果為null時,均會報出空引用異常。
接着算出key所應該對應的主數組的索引。循環周遊出該數組元素所對應的隊列(tab[index]),如果元素的hash值等于新添加元素的hash,同時entry的key等于(equals)key方法。則直接替換這個entry的value為參數傳入的value,與此同時傳回舊old。
如果整個循環都發現沒有,則說明目前hashtable其實并不存在該參數key,則調用剛才說的addEntry方法,将參數key value,及對應的索引傳進去。這裡注意put方法為同步公有方法,而addEntry為私有非同步方法,這裡是否存線上程安全問題呢?(防盜連接配接:本文首發自http://www.cnblogs.com/jilodream/ )其實并不存在,這是由于addEntry盡管會操作全局變量主數組,但是addEntry方法隻會被put方法調用。而凡是調用put方法的線程均需要先拿到this變量鎖。盡管再次進去無同步的addEntry的方法區,目前線程仍然持有this變量鎖,其他線程若想操作全局變量主數組,仍然需要等待全局鎖的釋放才可以。
接着是remove方法。該方法邏輯如下:首選根據key值計算出元素所對應主數組中的索引位置。然後依次循環主數組下該索引對應元素的後繼元素。判斷該元素的hash是否等于key參數的hash,以及元素是否equels參數key。如果相等,則将該元素從隊列中抹除。同時hashtable的長度count 減1,同時modCount值也自加1。如果循環結束仍未找到合适的元素與參數key相等,則傳回null
接着是putAll方法,該方法會将map集合中的對象,采用foreach的形式,依次調用put方法添加到hashtable集合中。
接着是clear方法。該方法首先将modCount加1.接着循環主數組中的所有元素,然後依次對這些元素置于null。然後設定count元素為0。這裡有一點需要注意,即clear時,隻清理了主數組中的元素。對于主數組中對應元素的後繼清單,則采用不予理會的态度。等待GC來回收掉。
接着是clone方法。該方法會克隆一個自身對象的副本。此方法會克隆出一個空的hashtable。然後将主數組中的所有元素克隆一遍,放置到克隆對象的對應位置上。注意在克隆元素的時候,會将元素的後繼隊列元素,依次的克隆下去。接着初始化克隆對象的其他變量:置空keyset、entryset、values對象,設定modCount為0。
然後是重寫的tostring方法。這個方法邏輯也很簡單,就是依次周遊元素。最後生成一個類似于{“key1”=”value1”,“key2”=”value2”}的結構。有趣的是這裡需要調用key.tostring,倘若key是目前hashtable自己的話,就直接使用“(this map)”字元串。防止出現無限遞歸。
到這裡hashtable的主要邏輯就已經都介紹完了。其餘還包括一些keyset、valueSet的内部類、以及replaceAll、putIfAbsent等封裝方法。由于代碼邏輯簡單,數量較大,這裡就不一一列舉了。
總結:
1、Hashtable包括tostirng等方法在,幾乎所有對外api方法都是同步保護的,這就是為什麼很多人認為hashtable線程安全的原因。而在基礎上,對于同步方法所調用的private方法,則大多采用非同步的形式。因為這些方法,往往隻有一個public方法可以調用,這樣就做到了在安全的基礎上可以更快執行代碼。
2、hashtable的内部結構大緻如下,和早前的hashmap很像:

3、關于元素的取值,hashtable不允許key和value取值為null。是以get時,發現為null,即說明key元素不存在。同時hashtable在擴容是采用的是乘2加1的方式。這與有些容器直接乘2有所差別。
4、關于變量modCount的使用。我們可以看到這個方法中,每次在發生增删改的時候都會出現modCount++的動作。而modcount可以了解為是目前hashtable的狀态。每發生一次操作,狀态就向前走一步。(防盜連接配接:本文首發自http://www.cnblogs.com/jilodream/ )設定這個狀态,主要是由于hashtable等容器類在疊代時,判斷資料是否過時時使用的。盡管hashtable采用了原生的同步鎖來保護資料安全。但是在出現疊代資料的時候,則無法保證邊疊代,邊正确操作。于是使用這個值來标記狀态。一旦在疊代的過程中狀态發生了改變,則會快速抛出一個異常,終止疊代行為(是以這種錯誤也叫做快速失敗fail—fast):


本文轉自 zddnd 51CTO部落格,原文連結:http://blog.51cto.com/13013666/1949239