HashMap源码解释
HashMap
前言:
本文的hashMap是基于jdk1.7的hashMap.
關于jdk1.8的hashMap在另一篇中,那里將會介紹與1.7的差異與優勢
首先基礎知識介紹:
1.HashMap的成員變量
int DEFAULT_INITIAL_CAPACITY = 16:默認的初始容量為2 ^ 4
int MAXIMUM_CAPACITY = 1 << 30:最大的容量為 2 ^ 30
float DEFAULT_LOAD_FACTOR = 0.75f:默認的加載因子為 0.75f
Entry< K,V>[] table:Entry類型的數組,HashMap用這個來維護內部的數據結構,它的長度由容量決定
int size:HashMap的大小
int threshold:HashMap的極限容量,擴容臨界點(容量和加載因子的乘積),默認為12
2.HashMap的構造函數
public HashMap():構造一個具有默認初始容量 (16) 和默認加載因子 (0.75) 的空 HashMap
public HashMap(int initialCapacity):構造一個帶指定初始容量和默認加載因子 (0.75) 的空 HashMap
public HashMap(int initialCapacity, float loadFactor):構造一個帶指定初始容量和加載因子的空 HashMap
public HashMap(Map< ? extends K, ? extends V> m):構造一個映射關系與指定 Map 相同的新 HashMap
3.HashMap的數據結構
要知道hashmap是什么,首先要搞清楚它的數據結構,在java編程語言中,最基本的結構就是兩種,一個是數組,另外一個是模擬指針(引用),所有的數據結構都可以用這兩個基本結構來構造的,hashmap也不例外。Hashmap實際上是一個數組和鏈表的結合體(在數據結構中,一般稱之為“鏈表散列“),請看下圖(縱排表示數組,橫排表示數組元素【實際上是一個鏈表】)。
從圖中我們可以看到一個hashmap就是一個數組結構,當新建一個hashmap的時候,就會初始化一個數組。
4.hashMap的put邏輯
首先put有兩個入參,我們擬定叫k和v,
提前申明上圖中的縱列,我們也叫hash表,也稱table,table[index]表示下標為index的bucket(這里的bucket可能是多個entry,比如上圖的第一行,有4個)
1).根據k算出hash值(這個hash方法,可以說是整個hashMap的精華所在,后面講)
2).根據這個hash值,算出index下標(這里算的地方很巧妙,Entry數組的大小規定為2的冪就是為了能夠使用這個算法來確定數組的下標,具體實現后面講)
3).如果找不到該下標對應的entry,就新建一個entry(新建entry涉及到擴容,后面講)
4),如果找到了該下標,就在對應的多個entry中找對應k的entry,找到就替換這個entry的value,找不到就新建一個entry
注意:hash表的初始化過程,并不是在new HashMap的時候執行的,而是在第一次push的時候執行的
5.hash表的初始化
初始化方法內部會重新計算Entry數組的容量,因為在構造HashMap時傳入的初始化大小可能不是2的冪(前面有提到,hashMap的構造函數,忘了快去看…),因此要將這個數轉換成2的冪再去根據新的容量新建Entry數組。初始化哈希表時再次重新設置閥值,閥值一般是capacity*loadFactor。
此外,在初始化哈希表時還會去初始化哈希種子(hashSeed),這個hashSeed用于優化哈希函數,默認為0是不使用替代哈希算法,但是也可以自己去設置hashSeed的值,以達到優化效果。
6.entry新建與擴容
新建一個Entry之前會先判斷當前集合元素的大小是否超過了閥值,如果超過了閥值并且當前entry所在位置不為空,就調用resize進行擴容。傳入的新的容量是原來哈希表的兩倍,在resize方法內部會新建一個容量為原先的2倍的Entry數組。然后將舊的哈希表里面的元素全部遷移到新的哈希表,其中可能會進行再哈希,根據initHashSeedAsNeeded方法計算的值來確定是否進行再哈希。完成哈希表的遷移之后,將當前哈希表替換為新的,最后再根據新的哈希表容量來重新計算HashMap的閥值。
很明顯,擴容步驟很多,操作很多,所以我們要合理的設置初始容量,盡量要避免這種擴容
7.如何根據hash值,算出index下標
indexFor方法是根據hash碼來計算出在數組中對應的下標。我們可以看到在這個方法內部使用了與(&)操作符。與操作是對兩個操作數進行位運算,如果對應的兩個位都為1,結果才為1,否則為0。與操作經常會用于去除操作數的高位值,例如:01011010 & 00001111 = 00001010。
已知傳入的length是Entry數組的長度,我們知道數組下標是從0開始計算的,所以數組的最大下標為length-1.如果length為2的冪,那么length-1的二進制位后面都為1.這時h&(length-1)的作用就是去掉了h的高位值,只留下h的低位值來作為數組的下標.由此可以看到Entry數組的大小規定為2的冪就是為了能夠使用這個算法來確定數組的下標.
8.計算hash
這個先上源碼
hash方法的最后兩行是真正計算hash值的算法,計算hash碼的算法被稱為擾動函數,所謂的擾動函數就是把所有東西雜糅到一起,可以看到這里使用了四個向右移位運算.目的就是將h的高位值與低位值混合一下,以此增加低位值的隨機性.在上面我們知道定位數組的下標是根據hash碼的低位值來確定的.key的hash碼是通過hashCode方法來生成的,而一個糟糕的hashCode方法生成的hash碼的低位值可能會有很大的重復.為了使得hash碼在數組上映射的比較均勻,擾動函數就派上用場了,把高位值的特性糅合進低位值,增加低位值的隨機性,從而使散列分布的更加松散,以此提高性能.下圖舉了個例子幫助理解.
9.hashMap的get邏輯
1).如果key為null,求null鍵
2).調用hash(key)求得key的hash值,然后調用indexFor(hash)求得hash值對應的table的索引位置,然后遍歷索引位置的鏈表,如果存在key,則把key對應的Entry返回,否則返回null
**注意:**這里經常會有人問:如果兩個key的hashcode一樣,那么會怎么取.其實只要記住,相同的hashcode的entry會放在同一個位置,這個位置可能會有多個entry形成鏈表,每個entry存儲key,value值,所以再用key找到對應的entry,就能拿到真正要的value了.
其實,在整個get方法中,key是被用了兩次,一次是用來計算hash值,為了找到bucket位置(哪一行(每行有多個value)),另外一次是找到某行后,用在尋找具體的某一個entry,從而拿到真正的value.
10.為什么hashMap是線程不安全的
一句話解釋:當多線程的情況下,擴容過程可能產生條件競爭(race condition),可能會帶來循環鏈表,導致死循環致使線程掛掉.
慢慢解釋:如果兩個線程都發現HashMap需要重新調整大小了,它們會同時試著調整大小.在調整大小的過程中,存儲在鏈表中的元素的次序會反過來,因為移動到新的bucket位置的時候,HashMap并不會將元素放在鏈表的尾部,而是放在頭部,這是為了避免尾部遍歷(tail traversing).如果條件競爭發生了,就可能存在鏈表末尾的元素的next指針指向了鏈表頭,循環鏈表就出現了.按道理,HashMap是不存在循環鏈表的,當我們調用get()這個鏈表中不存在的元素的時候,那么就死循環了.
注:hashTable是線程安全的,但它并未使用分段鎖,而是鎖住整個數組,高并發環境下效率非常的低,會導致大量線程等待.因此并發環境下,建議使用Java.util.concurrent包中的ConcurrentHashMap以保證線程安全.
最后附帶成員變量源碼的簡單解釋(可自己參照源碼對比,效果更好)
/*** The default initial capacity - MUST be a power of two.* 默認初始容量(16)*/static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16/*** The maximum capacity, used if a higher value is implicitly specified* by either of the constructors with arguments.* MUST be a power of two <= 1<<30.* 默認最大容量*/static final int MAXIMUM_CAPACITY = 1 << 30;/*** The load factor used when none specified in constructor.* 默認加載因子, 指哈希表可以達到多滿的尺度*/static final float DEFAULT_LOAD_FACTOR = 0.75f;/*** An empty table instance to share when the table is not inflated.* 空的哈希表*/static final Entry<?,?>[] EMPTY_TABLE = {};/*** The table, resized as necessary. Length MUST Always be a power of two.* 實際使用的哈希表* 其實是一個Entry數組,Entry是HashMap的靜態內部類*/transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;/*** The number of key-value mappings contained in this map.* HashMap大小, 即HashMap存儲的鍵值對數量*/transient int size;/*** The next size value at which to resize (capacity * load factor).* 鍵值對的閾值, 用于判斷是否需要擴增哈希表容量* 默認是初始容量*加載因子,也就是16*0.75=12* 當鍵值對超過閾值,會觸發自動擴容機制*/// If table == EMPTY_TABLE then this is the initial capacity at which the// table will be created when inflated.int threshold;/*** The load factor for the hash table.* 加載因子*/final float loadFactor;/*** The number of times this HashMap has been structurally modified* Structural modifications are those that change the number of mappings in* the HashMap or otherwise modify its internal structure (e.g.,* rehash). This field is used to make iterators on Collection-views of* the HashMap fail-fast. (See ConcurrentModificationException).* 修改次數, 用于fail-fast機制*/transient int modCount;/*** The default threshold of map capacity above which alternative hashing is* used for String keys. Alternative hashing reduces the incidence of* collisions due to weak hash code calculation for String keys.* <p/>* This value may be overridden by defining the system property* {@code jdk.map.althashing.threshold}. A property value of {@code 1}* forces alternative hashing to be used at all times whereas* {@code -1} value ensures that alternative hashing is never used.* 使用替代哈希的默認閥值*/static final int ALTERNATIVE_HASHING_THRESHOLD_DEFAULT = Integer.MAX_VALUE;/*** A randomizing value associated with this instance that is applied to* hash code of keys to make hash collisions harder to find. If 0 then* alternative hashing is disabled.* 隨機的哈希種子, 有助于減少哈希碰撞的次數*/transient int hashSeed = 0; 創作挑戰賽新人創作獎勵來咯,堅持創作打卡瓜分現金大獎總結
以上是生活随笔為你收集整理的HashMap源码解释的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 搭建: canal部署与实例运行
- 下一篇: 获取iOS任意线程调用堆栈(二)符号化理