HashMap的工作原理


  

  HashMap基於hashing原理,我們通過put()get()方法儲存和獲取對象。當我們將鍵值對傳遞給put()方法時,它調用鍵對象的hashCode()方法來計算hashcode,讓后找到bucket位置來儲存值對象。當獲取對象時,通過鍵對象的equals()方法找到正確的鍵值對,然后返回值對象。HashMap使用鏈表來解決碰撞問題,當發生碰撞了,對象將會儲存在鏈表的下一個節點中。 HashMap在每個鏈表節點中儲存鍵值對對象。

 

1.HashMap介紹

 

  HashMapMap接口的一個實現類,實現了所有Map的操作。HashMap除了允許keyvalue保存null值和非線程安全外,其他實現幾乎和HashTable一致。

 

  HashMap使用散列存儲的方式保存kay-value鍵值對,因此其不支持數據保存的順序。如果想要使用有序容器可以使用LinkedHashMap

 

  在性能上當HashMap中保存的key的哈希算法能夠均勻的分布在每個bucket中的是時候,HashMap在基本的getset操作的的時間復雜度都是O(n)

 

  在遍歷HashMap的時候,其遍歷節點的個數為bucket的個數+HashMap中保存的節點個數。因此當遍歷操作比較頻繁的時候需要注意HashMap的初始化容量不應該太大。 這一點其實比較好理解:當保存的節點個數一致的時候,bucket越少,遍歷次數越少。

 

  另外HashMapresize的時候會有很大的性能消耗,因此當需要在保存HashMap中保存大量數據的時候,傳入適當的默認容量以避免resize可以很大的提高性能。 具體的resize操作請參考下面對此方法的分析

 

  HashMap是非線程安全的類,當作為共享可變資源使用的時候會出現線程安全問題。需要使用線程安全容器:

 

  Map m = new ConcurrentHashMap();或者

 

  Map m = Collections.synchronizedMap(new HashMap());

 

  

2.數據結構介紹

 

  HashMap使用數組+鏈表+樹形結構的數據結構。

 

3.HashMap源碼分析(基於JDK1.8

 

3.1關鍵屬性分析

 

transient Node<K,V>[] table; //Node類型的數組,記我們常說的bucket數組,其中每個元素為鏈表或者樹形結構

 

transient int size;//HashMap中保存的數據個數

 

int threshold;//HashMap需要resize操作的閾值

 

final float loadFactor;//負載因子,用於計算threshold。計算公式為:threshold = loadFactor * capacity

 

其中還有一些默認值得屬性,有默認容量2^4,默認負載因子0.75.用於構造函數沒有指定數值情況下的默認值。

 

 3.2構造函數分析

 

HashMap提供了三個不同的構造函數,主要區別為是否傳入初始化容量和負載因子。分別文以下三個。

 

//此構造函數創建一個空的HashMap,其中負載因子為默認值0.75

 

public HashMap() {

 

    this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted

 

}

 

//傳入默認的容量大小,創造一個指定容量大小和默認負載因子為0.75HashMap

 

 public HashMap(int initialCapacity) {

 

    this(initialCapacity, DEFAULT_LOAD_FACTOR);

 

}

 

//創建一個指定容量和指定負載因為HashMap,以下代碼刪除了入參檢查

 

public HashMap(int initialCapacity, float loadFactor) {

 

    this.loadFactor = loadFactor;

 

    this.threshold = tableSizeFor(initialCapacity);

 

}

 

注意:此處的initialCapacity為數組table的大小,即bucket的個數。

 

其中在指定初始化容量的時候,會根據傳入的參數來確定HashMap的容量大小。

 

初始化this.threshold的值為入參initialCapacity距離最近的一個2n次方的值。取值方法如下:

 

case initialCapacity = 0:  

 

     this.threshold = 1;       

 

case initialCapacity為非0且不為2n次方:  

 

    this.threshold = 大於initialCapacity中第一個2n次方的數。  

 

case initialCapacity = 2^n:  

 

    this.threshold = initialCapacity

 

具體的計算方法為tableSizeFor(int cap)函數。計算方法是將入參的最高位下面的所有位都設置為1,然后加1

 

下面以入參為134217729為例分析計算過程。

 

首先將int轉換為二進制如下:

 

cap = 0000 1000 0000 0000 0000 0000 0000 0001

 

另外此處賦值為this.threshold,是因為構造函數的時候並不會創建table,只有實際插入數據的時候才會創建。目的應該是為了節省內存空間吧。

 

在第一次插入數據的時候,會將tablecapacity設置為threshold,同時將threshold更新為loadFactor * capacity

 

3.3關鍵函數源碼分析

 

3.3.1 第一次插入數據的操作

 

HashMap在插入數據的時候傳入key-value鍵值對。使用hash尋址確定保存數據的bucket。當第一次插入數據的時候會進行HashMap中容器的初始化。具體操作如下:

 

Node<K,V>[] tab;


        int n, i;


        if ((tab = table) == null || (n = tab.length) == 0)


            n = (tab = resize()).length;

 

 其中resize函數的源碼如下,主要操作為根據caploadFactory創建初始化table

 

Node<K, V>[] oldTab = table;


    int oldThr = threshold;  //oldThr 根據傳入的初始化cap決定 2的n次方


    int newCap, newThr = 0;


    if (oldThr > 0) // 當構造函數中傳入了capacity的時候


        newCap = oldThr;  //newCap = threshold  2的n次方,即構造函數的時候的初始化容量


     else {               // zero initial threshold signifies using defaults


        newCap = DEFAULT_INITIAL_CAPACITY;


        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);


    }


    float ft = (float)newCap * loadFactor;  // 2的n次方 * loadFactory


    newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?


            (int)ft : Integer.MAX_VALUE);


    threshold = newThr; //新的threshold== newCap * loadFactory


    Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; //長度為2的n次方的數組


    table = newTab;

 

在初始化table之后,將數據插入到指定位置,其中bucket的確定方法為:

 

i = (n - 1) & hash // 此處n-1必定為 0000 1111 1111....的格式,取&操作之后的值一定在數組的容量范圍內。


其中hash的取值方式為:


 static final int hash(Object key) {


    int h;


    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);


}

 

具體操作如下,創建Node並將node放到table的第i個元素中

 

if ((p = tab[i = (n - 1) & hash]) == null)


    tab[i] = new Node(hash, key, value, null);

 

3.3.2 非第一次插入數據的操作源碼分析

 

HashMap中已有數據的時候,再次插入數據,會多出來在鏈表或者樹中尋址的操作,和當size到達閾值時候的resize操作。多出來的步驟如下:

 

另外,在resize操作中也和第一次插入數據的操作不同,當HashMap不為空的時候resize操作需要將之前的數據節點復制到新的table中。操作如下:

 

3.4CloneableSerializable分析

 

HashMap的定義中實現了Cloneable接口,Cloneable是一個標識接口,主要用來標識 Object.clone()的合法性,在沒有實現此接口的實例中調用 Object.clone()方法會拋出CloneNotSupportedException異常。可以看到HashMap中重寫了clone方法。

 

HashMap實現Serializable接口主要用於支持序列化。同樣的Serializable也是一個標識接口,本身沒有定義任何方法和屬性。另外HashMap自定義了

 

private void writeObject(java.io.ObjectOutputStream s) throws IOException


private void readObject(java.io.ObjectInputStream s) throws IOException, ClassNotFoundException

 

兩個方法實現了自定義序列化操作。

 

注意:支持序列化的類必須有無參構造函數。這點不難理解,反序列化的過程中需要通過反射創建對象。

4.HashMap的遍歷

以下討論兩種遍歷方式,測試代碼如下:

方法一:

通過map.keySet()獲取key的集合,然后通過遍歷key的集合來遍歷map

方法二:

通過map.entrySet()方法獲取map中節點集合,然后遍歷此集合遍歷map

測試代碼如下:

public static void main(String[] args) throws Exception {

        Map<String, Object> map = new HashMap<>();

        map.put("name", "test");

        map.put("age", "25");

        map.put("address", "HZ");

        Set<String> keySet = map.keySet();

        for (String key : keySet) {

            System.out.println(map.get(key));

        }

        Set<Map.Entry<String, Object>> set = map.entrySet();

        for (Map.Entry<String, Object> entry : set) {

            System.out.println("key is : " + entry.getKey() + ".  value is " + entry.getValue());

        }

    }

 

HashMap 1.7 與 1.8 的 區別,說明 1.8 做了哪些優化,如何優化的?

Hashmap的結構,1.71.8有哪些區別

不同點:

1JDK1.7用的是頭插法,而JDK1.8及之后使用的都是尾插法,那么他們為什么要這樣做呢?因為JDK1.7是用單鏈表進行的縱向延伸,當采用頭插法時會容易出現逆序且環形鏈表死循環問題。但是在JDK1.8之后是因為加入了紅黑樹使用尾插法,能夠避免出現逆序且鏈表死循環的問題。

2)擴容后數據存儲位置的計算方式也不一樣:

1. JDK1.7的時候是直接用hash值和需要擴容的二進制數進行&(這里就是為什么擴容的時候為啥一定必須是2的多少次冪的原因所在,因為如果只有2n次冪的情況時最后一位二進制數才一定是1,這樣能最大程度減少hash碰撞)(hash& length-1

2、而在JDK1.8的時候直接用了JDK1.7的時候計算的規律,也就是擴容前的原始位置+擴容的大小值=JDK1.8的計算方式,而不再是JDK1.7的那種異或的方法。但是這種方式就相當於只需要判斷Hash值的新增參與運算的位是0還是1就直接迅速計算出了擴容后的儲存方式。

3JDK1.7的時候使用的是數組+ 單鏈表的數據結構。但是在JDK1.8及之后時,使用的是數組+鏈表+紅黑樹的數據結構(當鏈表的深度達到8的時候,也就是默認閾值,就會自動擴容把鏈表轉成紅黑樹的數據結構來把時間復雜度從On)變成OlogN)提高了效率)

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM