Java基礎--HashTable源碼


HashTable的類圖
在這裏插入圖片描述
可以看到HashTable繼承於Dictionary實現了Map、Cloneable、Serializable接口。
其中Cloneable和Serializable是標記性接口。標記性接口,就是說接口裏面沒有定義任何的方法,一個類實現接口,也不需要實現任何的方法。這些接口存在意義只是標識,這些類可以做什麼操作。
這裏討論的主要是HashTable、Dictionary和Map接口。
所以,暫時略過Cloneable和Serializable接口。

1.Map接口

Map接口實現了泛型,接受兩個類型。
在這裏插入圖片描述
在這裏插入圖片描述
這是Map接口定義的方法:(我們這裏只關注下jdk8新增的方法,其他的方法暫不關注。特別是默認實現的方法)

  • getOrDefault 這是默認方法,如果存在指定鍵的值,那麼就返回值,否則返回默認值。
  • forEach 默認方法,傳入一個BiConsumer,內部實現是增強for循環。
  • replaceAll 默認方法,傳入BiFunction.增強for循環實現。
  • putIfAbsent 默認方法,傳入key,value。如果不存在指定的鍵值對,那麼就將鍵值對放入map.
    會先調用get方法,如果get結果爲空,然後在put.
  • remove默認方法,如果根據鍵得到的值爲空或者與指定的值不同,或者不存在鍵,那麼會移除失敗。
    移除成功返回true。
  • replace 更新指定鍵的值,即使指定的鍵與預期的舊值不同,也會更新爲新值。指定鍵如果不存在,覆蓋失敗。
  • replace 指定的鍵不存在或者指定的鍵對應的值不存在,會replace失敗。
  • computeIfAbsent 如果指定的鍵對應的值不存在,會將指定的鍵進行運算後的值,作爲指定鍵的值。如果指定的鍵對應的值存在,那麼就返回已有的值。
  • computeIfPresent 指定的鍵值對存在,那麼對指定的鍵值對進行運算。如果運算的結果爲空,那麼移除指定的鍵值對;如果運算的結果不爲空,那麼將運算得到的新值加入到map中。
  • compute 根據指定的鍵值對進行運算,如果運算結果爲空,且map中存在指定的鍵對應的鍵值對,那麼就移除此鍵值對。如果指定的鍵對應的舊值爲空,那麼什麼也不做。如果運算的新值不爲空,那麼將將新值作爲此鍵的值。不存在則添加,存在則覆蓋。
  • merge 如果指定的鍵對應的值爲空,那麼將傳入的值作爲最終值,如果最終值爲空,那麼移除鍵,如果最終結果不爲空,那麼覆蓋原值。如果指定的鍵對應的值不爲空,那麼將舊值和傳入的值,代入式子進行計算,計算結果作爲最終值,如果最終值爲空,那麼移除鍵,否則增加或覆蓋原值。

Entry作爲Map裏面元素的鍵值對數據結構,接口定義了基本的方法。
Entry的訪問控制是默認權限,也就是包內。
這裏看下jdk8增加的默認方法:

  • comparingByKey返回一個比較器。比較器是將鍵值對的鍵按照自然順序進行排序。
  • comparingByValue返回一個比較器。比較器是將鍵值對的值按照自然順序進行排序的。
  • comparingByKey根據傳入的比較,返回一個比較器。默認是自然順序,這個是指定排序規則。
  • comparingByValue根據傳入的比較,返回一個比較器。默認是自然順序,這個是指定排序規則。

2.Dictionary

在這裏插入圖片描述
Dictionary是一個抽象類,除了默認的無參構造,其餘方法都是抽象的。(爲什麼不做成接口呢?)

3.HashTable

HashTable集成了Dictonary,實現了Map接口。
所以,在1,2中的抽象方法和接口中的方法,都需要實現。

3.1 全局屬性

在這裏插入圖片描述
數據核心的存儲結構是一個不序列化的Entry數組

private transient Entry<?,?>[] table;
  • table是數據存儲的數組;
  • count是記錄數組中元素的個數
  • threshold是表示需要擴容的閾值。閾值 = 容器容量*擴容因子。
  • loadFactor是擴容因子。一般默認0.75
  • modCount是修改記錄版本。
  • keySet內存可見,用volatitle修飾
  • entrySet內存可見
  • values也是內存可見

3.2 輔助類

在這裏插入圖片描述

3.2.1 Entry

內部私有靜態類,實現了Map中的Entry接口。
全局屬性:
final int hash;
final K key;
V value;
Entry<K,V> next;
看到這裏,可以看出,HashTable的存儲結構是鏈表。
數據結構類似下圖
在這裏插入圖片描述
Entry的hashCode方法是:key的hashCode和value的hashCode進行按位與。
這個Entry的hashCode方法主要涉及計算整個HashTable的hashCode。
HashTable的hashCode方法是將存儲結構內所有的鍵值對的hashCode進行累加,累加結果作爲HashTable的hashCode返回。
也就是
在這裏插入圖片描述

3.2.2 Enumerator

這是HashTable的核心遍歷器。
其全局屬性:

  • table hashTable的數組(哈希桶數組)。
  • index hashTable的當前操作的數組元素(當前的哈希桶,默認爲數組結尾)
  • entry hashTable的元素(哈希桶元素)。
  • lastReturned 當前的hashTable的元素
  • type 遍歷模式
  • iterator 是否可遍歷
  • expectedModCount 開始遍歷的版本
    遍歷器有一個兩參數的構造方法:模式,是否可遍歷。
    模式共有3個模式:key,values,entries
    分別遍歷鍵、值、元素
    方法:
  • hasMoreElements是否存在前一個hashTable數組元素(是否存在前一個哈希桶)(從後向前)
  • nextElement 根據遍歷模式,返回模式對應的元素的值。
  • hasNext 代理指向hasMoreElements
  • next 判斷當前的版本與開始遍歷的版本是否相同,不同則拋出ConcurrentModificationException異常,相同則代理指向nextElement
  • remove 驗證可遍歷,當前遍歷元素不爲空,當前版本與開始遍歷版本相同。進入synchronized代碼塊:當前遍歷元素的hashCode計算出當前元素的table位置:hash & 0x7fffffff % table.length;然後在哈希桶中找到此元素,然後從鏈表結構中取出當前遍歷的元素,然後將當前遍歷的元素置空。最後會將開始遍歷的版本和當前版本加1。

3.2.3 KeySet

在這裏插入圖片描述
KeySet繼承抽象類AbstractSet
實現了一些抽象方法。
這些抽象方法都是代理,指向HashTable的方法或者是迭代器的方法。

3.2.4 ValueCollection

在這裏插入圖片描述
ValueCollection繼承抽象類AbstractCollection
實現了一些抽象方法。
這些抽象方法都是代理,指向HashTable的方法或者是迭代器的方法。

3.2.5 EntrySet

在這裏插入圖片描述
在這裏插入圖片描述
EntrySet也是在Enumerator的基礎上封裝形成的,一些其他的方法還是調用HashTable自己實現的方法。

3.2.6 關係

將輔助類進行分類,可以根據其功能,分爲:存儲節點Entry,Enumartor迭代器,KeySet,EntrySet,ValueCollection是迭代器代理。

3.3 HashTable方法

3.3.1 HashTable構造

HashTable的構造方法可以指定初始大小,也能指定擴容因子,但是不能指定初始擴容閾值。
在這裏插入圖片描述
從構造方法可以看出,不能創建空的HashTable,即使你指定初始默認大小是0,也會修改爲1.
而且,HashTable在創建的時候,就會將數組空間分配。

注意,這裏的初始大小,不是元素的個數的大小,而是初始的哈希桶或者說哈希槽的個數。擴容因子是針對哈希桶進行擴容的乘數因子。但是擴容閾值,卻與HashTable中元素的個數有關。
初始的閾值是哈希槽的數量的擴容因子倍。
在這裏插入圖片描述
擴容因子默認是0.75.
也就是說,HashTable的擴容是75%擴容的。(哈希槽擴容,注意和元素可存儲的個數的擴容不同)
ArrayList是100%擴容。(移位實現)
Vector也是100%擴容。(Vector還可以定義不可擴容)
LinkedList不存在擴容問題。
在這裏插入圖片描述
HashTable如果不指定初始哈希槽的大小,那麼可以存放11*0.75=8.25即8個元素。
在這裏插入圖片描述
根據已有集合創建hashTable的時候,如果集合的元素個數的2倍大於11個。那麼將哈希槽設置爲集合元素數量個,否則就設置爲11個。
所以,HashTable的默認初始哈希槽的大小就是11.

3.3.2 HashTable的普通方法

在這裏插入圖片描述
HashTable的方法是線程安全的。不過這個線程安全是通過synchronized的關鍵字實現的。在以前的jdk版本,synchronized的性能比較差,不過隨着jdk對synchronized的性能的優化,使用到synchronized的性能影響越來越小了。

3.3.3 HashTable的迭代方法

在這裏插入圖片描述
可以發現,通過創建迭代器的模式不同,實現不同屬性的遍歷。

3.3.4 contains方法

在這裏插入圖片描述
在內部,contains方法是通過雙重for循環實現的。
所以,這個方法的性能也好不到哪去。
不過,用起來很方便。

3.3.5 containsKey方法

通過HashTable的結構,因爲所有元素都是在一個個的哈希槽下面的,而哈希槽的哈希值是通過key的hashCode計算的。所以,判斷一個key是否存在,我們只需要重新計算key的hashCode即可。
在這裏插入圖片描述

3.3.6 get方法

在這裏插入圖片描述
get方法是通過計算key的hash值,找到key對應的哈希槽,然後遍歷哈希槽內的元素鏈表。如果key的hashCode和key都相等,就返回這個key對應的value.

3.3.7 put方法

在這裏插入圖片描述
其put方法首先會判斷value是否爲空,結合hashTable每次操作都是根據key,hashCode進行的。可以推斷出,hashTable不管是鍵還是值,都不允許爲空。
在加入hashTable前,先根據key的哈希值,找到對應的哈希槽,然後遍歷哈希槽中鏈表的數據,看看我們要加入的數據是否已經存在。也就是說,hashTtable不允許鍵值同時存在重複。而且,根據其put的邏輯,如果鍵相同,那麼會將新值覆蓋舊值,然後返回舊值。

3.3.8 addEntry

如果鍵值都不相同,那麼就需要增加元素:
在這裏插入圖片描述
在增加元素的時候,首先會將版本號加1.
然後判斷現有數量是否達到擴容閾值。如果到達擴容閾值,就行重新哈希,進行擴容。然後將待加入的元素加入。
如果沒到達閾值,如果已有哈希槽,那麼就使用鏈表的頭插法,將待加入元素加入此哈希槽下的鏈表頭部,然後將新加入的鏈表頭放到哈希槽。
如果沒有哈希槽,那麼新創建的哈希槽只有一個元素,這個元素就是鏈表的頭,而且鏈表頭在哈希槽。

3.3.9 rehash

重新哈希:
在這裏插入圖片描述
在重新哈希的時候,會將現有的哈希槽的數量擴大一倍+1.
然後更新版本號,更新擴容的閾值。
然後將舊的hastTable中的哈希槽進行重新哈希,然後將舊哈希槽對應的鏈表的鏈表頭賦值給新哈希槽。
實現hashTable的擴容。

3.3.10 clear

在這裏插入圖片描述
clear方法是將每個哈希槽對應的鏈表清空。

3.3.11 clone

在這裏插入圖片描述
克隆方法會循環克隆當前的hashTable的屬性,不過會清空版本號。

3.3.12 迭代訪問

在這裏插入圖片描述因爲內部的迭代器都是私有的,爲了實現外界使用迭代器,需要暴露方法,這個暴露方法就是獲取內部私有的迭代器。
外界持有的是接口的實例。
hashTable內部確實是私有類的迭代器,但是其實現的接口是共有的。
外界需要共有接口的實例,此時內部通過上轉型,將私有內部類的實例,突破權限控制,暴露給外界,然後進行遍歷。

3.3.13 hashCode

hashTable內部,幾乎所有的操作都需要用到key的hashCode.那麼hashTable本身的hashCode是如何計算的呢?
在這裏插入圖片描述
是對hashTable內每一個元素都進行hashCode的計算,然後得出的hashCode.
從其實現的過程,我們可以發現對於hashTable的嵌套使用,性能比較差,特別是將hashTable作爲HashTable的key的時候,其性能會差到極點。

3.3.14 getOrDefault

在這裏插入圖片描述
獲取指定鍵的值,如果不存在,返回默認值。

3.3.15 forEach

在這裏插入圖片描述
發現forEach中會對開始遍歷的版本和當前的版本進行比較,只要不同就會通過拋出異常快速失敗。
所以,在forEach中不能進行put操作,也不能進行remove操作。
但是可以進行更新操作,因爲更新操作不會修改版本號。

3.3.16 remove

在這裏插入圖片描述
在remove時,會將版本號加1,所以,remove前後,版本號變了。

3.3.17 replace

在這裏插入圖片描述
replace前後,版本號也是不變的。

3.3.18 writeObject

在這裏插入圖片描述
這個方法是在序列化寫的時候用到,對應的還有序列化讀的方法。

3.3.19 readObject

在這裏插入圖片描述

4. 總結

看了這麼多的for循環,發現在hashTable中,好多的for循環都是從後向前的。
如果這個for循環給我來寫,可能我寫的是這樣的:

for(int i = table.length - 1 ; i >= 0; i--){
//......
}

但是hashTable裏面的寫法,卻比我寫的少一些:

for(int i = table.length;--i>=0;){
//......
}

從這裏來看,至少少寫了 i–, - 1
對於我們一般的遍歷,這可能沒有什麼操作,但是對於集合框架來說,少寫2個代碼單詞,可能對於所有使用集合的人來說,節省了時間。
有時候,我也在想,我們看jdk的源碼的目的是什麼?
瞭解一些非常cool的api,然後在工作中cool的使用出來?
爲了讓其他人在問你時,你可以避免被刨根問底?
爲了面試?
可能都有吧。
但是我覺得至少還有一點,作爲java開發人員,或者說現在大多數的系統都是基於java語言實現,那麼,jdk相當於是我的核心武器。作爲一個士兵,我應該對自己的武器非常熟悉纔對。
作爲武器,我應該比較瞭解,不僅僅是瞭解其使用,也是瞭解這個武器的一些原理。
摸清這個武器的實現過程,嘗試理解這個武器的優劣。
最終的目的是根據對核心武器的使用,能夠創造出更加適合自己的武器。
從而讓自己的能力不斷提高。
當然,這只是一個java碼農對自己的一個小小的認知與規劃吧。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章