hashMap源碼中那些你不注意的事

  首先來說一個非常形象的例子,來說明下數組和鏈表。


  上體育課的時候,老師說:你們站一隊,每個人記住自己是第幾個,我喊到幾,那個人就舉手,這就是數組
  老師說,你們每個人記住自己前面的人和後面的人,然後老師只知道第一人是誰。 然後你們各自由活動,老師要找某一個人,是不是每次都是從第一個開始往自己身後的人開始傳達?這就是鏈表
  老師說: 大家1,2,3,4報數,凡是報1,爲1隊,凡是報2的爲2隊.......  這就是散列(哈希)。而這個4就相當於預定義好的桶的個數。

 

程序中,存放指定的數據最常用的數據結構有兩種:數組和鏈表。

數組和鏈表的區別:
  1、  數組是將元素在內存中連續存放。
         鏈表中的元素在內存中不是順序存儲的,而是通過存在元素中的指針聯繫到一起。
  2、  數組必須事先定義固定的長度,不能適應數據動態的增減的情況。當數據增加時,可能超出原先定義的元素個數;當數據減少時,造成內存浪費;
    鏈表動態地進行存儲分配,可以適應數據動態地增減的情況。
  3、(靜態)數組從棧中分配空間,對於程序員方便快速,但是自由度小;
    鏈表從堆中分配空間,自由度大但是申請管理比較麻煩。

 


數組和鏈表在存儲數據方面到底誰好?根據數組和鏈表的特性,分兩種情況討論:
  1、當進行數據查詢時,數組可以直接通過下標迅速訪問數組中的元素。
        而鏈表則需要從第一個元素開始一直找到需要的元素位置,
        顯然,數組的查詢效率會比鏈表的高。

  2、當進行增加或刪除元素時,在數組中增加一個元素,需要移動大量元素,在內存中空出一個元素的空間,然後將要增加的元素放在其中。

             同樣,如果想刪除一個元素,需要移動大量去填掉被移動的元素,而鏈表只需改動元素中的指針即可實現增加或刪除元素。
 

 

 


  那麼哈希表,是既能具備數組的快速查詢的優點,又能融合鏈表方便快捷的增加刪除元素的優勢。
  所謂的hash,簡單的說就是散列,即將輸入的數據通過hash函數得到一個key值,輸入的數據存儲到數組中下標的key值的數組單元中去。 

 

 

 

 

  Java中數據存儲方式最底層的兩種結構,一種是數組,另一種就是鏈表。

  數組的特點:連續空間,尋址迅速,但是在刪除或者添加元素的時候需要有較大幅度的移動,所以查詢速度快,增刪較慢。

  而鏈表正好相反,由於空間不連續,尋址困難,增刪元素只需修改指針,所以查詢慢、增刪快。

  有沒有一種數據結構來綜合一下數組和鏈表,以便發揮他們各自的優勢?答案是肯定的!就是:哈希表

  哈希表具有較快(常量級)的查詢速度,及相對較快的增刪速度,所以很適合在海量數據的環境中使用。一般實現哈希表的方法採用“拉鍊法”,我們可以理解爲“鏈表的數組”,如下圖:

  哈希表。如拿HashMap來說。

 

  從上圖中,我們可以發現哈希表是由數組  +   鏈表組成的,一個長度爲16的數組中,每個元素存儲的是一個鏈表的頭結點。那麼這些元素是按照什麼樣的規則存儲到數組中呢。一般情況是通過hash(key)%len獲得,也就是元素的key的哈希值對數組長度取模得到。比如上述哈希表中,12%16=12,28%16=12,108%16=12,140%16=12。所以12、28、108以及140都存儲在數組下標爲12的位置。它的內部其實是用一個Entity數組來實現的,屬性有key、value、next。

 

在Java 8 之前,HashMap和其他基於map的類都是通過鏈地址法解決衝突,它們使用單向鏈表來存儲相同索引值的元素。在最壞的情況下,這種方式會將HashMap的get方法的性能從O(1)降低到O(n)。爲了解決在頻繁衝突時hashmap性能降低的問題,Java 8中使用平衡樹來替代鏈表存儲衝突的元素。這意味着我們可以將最壞情況下的性能從O(n)提高到O(logn)。在Java 8中使用常量TREEIFY_THRESHOLD來控制是否切換到平衡樹來存儲。目前,這個常量值是8,這意味着當有超過8個元素的索引一樣時,HashMap會使用樹來存儲它們。這一改變是爲了繼續優化常用類。在Java 7中爲了優化常用類對ArrayList和HashMap採用了延遲加載的機制,在有元素加入之前不會分配內存,這會減少空的鏈表和HashMap佔用的內存。這一動態的特性使得HashMap一開始使用鏈表,並在衝突的元素數量超過指定值時用平衡二叉樹替換鏈表。不過這一特性在所有基於hash table的類中並沒有,例如Hashtable和WeakHashMap。目前,只有ConcurrentHashMap,LinkedHashMapHashMap會在頻繁衝突的情況下使用平衡樹。

什麼時候會產生衝突

HashMap中調用hashCode()方法來計算hashCode。由於在Java中兩個不同的對象可能有一樣的hashCode,所以不同的鍵可能有一樣hashCode,從而導致衝突的產生。

總結

HashMap在處理衝突時使用鏈表存儲相同索引的元素。
從Java 8開始,HashMap,ConcurrentHashMap和LinkedHashMap在處理頻繁衝突時將使用平衡樹來代替鏈表,當同一hash桶中的元素數量超過特定的值便會由鏈表切換到平衡樹,這會將get()方法的性能從O(n)提高到O(logn)。
當從鏈表切換到平衡樹時,HashMap迭代的順序將會改變。不過這並不會造成什麼問題,因爲HashMap並沒有對迭代的順序提供任何保證。
從Java 1中就存在的Hashtable類爲了保證迭代順序不變,即便在頻繁衝突的情況下也不會使用平衡樹。這一決定是爲了不破壞某些較老的需要依賴於Hashtable迭代順序的Java應用。
除了Hashtable之外,WeakHashMap和IdentityHashMap也不會在頻繁衝突的情況下使用平衡樹。
使用HashMap之所以會產生衝突是因爲使用了鍵對象的hashCode()方法,而equals()和hashCode()方法不保證不同對象的hashCode是不同的。需要記住的是,相同對象的hashCode一定是相同的,但相同的hashCode不一定是相同的對象。
在HashTable和HashMap中,衝突的產生是由於不同對象的hashCode()方法返回了一樣的值。

以上就是Java中HashMap如何處理衝突。
JDK8之前,這種方法被稱爲鏈地址法,因爲使用鏈表存儲同一桶內的元素。通常情況HashMap,HashSet,LinkedHashSet,LinkedHashMap,ConcurrentHashMap,HashTable,IdentityHashMap和WeakHashMap均採用這種方法處理衝突。
從JDK 8開始,HashMap,LinkedHashMap和ConcurrentHashMap爲了提升性能,在頻繁衝突的時候使用平衡樹來替代鏈表。因爲HashSet內部使用了HashMap,LinkedHashSet內部使用了LinkedHashMap,所以他們的性能也會得到提升。

 

 

HashMap擴容

前言:

HashMap的size大於等於(容量*加載因子)的時候,會觸發擴容的操作,這個是個代價不小的操作。

爲什麼要擴容呢?HashMap默認的容量是16,隨着元素不斷添加到HashMap裏,出現hash衝突的機率就更高,那每個桶對應的鏈表就會更長,

這樣會影響查詢的性能,因爲每次都需要遍歷鏈表,比較對象是否相等,一直到找到元素爲止。

爲了提升查詢性能,只能擴容,減少hash衝突,讓元素的key儘量均勻的分佈。

擴容基本點

加載因子默認值是0.75

1

static final float DEFAULT_LOAD_FACTOR = 0.75f;

容量的默認值是16

1

static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 等於16

HashMap提供了一個構造參數,可以在創建的時候指定容量和加載因子。

1

public HashMap(int initialCapacity, float loadFactor)

默認的情況下,HashMap 的size一旦大於等於16*0.75=12的話,

同時每個Entry(或者叫桶)裏面至少有一個元素的時候就會進行擴容。

1

2

3

4

5

if ((size >= threshold) && (null != table[bucketIndex])) {

      resize(2 * table.length);

      hash = (null != key) ? hash(key) : 0;

      bucketIndex = indexFor(hash, table.length);

}

擴容的時候,容器容量翻倍

1

resize(2 * table.length);

擴容的時候需要重新計算元素的數組下標

1、重新分配一個新的Entry數組
2、重新計算原來元素的在新數組中的下標(比較耗資源)

 

另外有一個比較的文章參考:

https://blog.csdn.net/luanlouis/article/details/41576373?utm_source=tuicool&utm_medium=referral 

 

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