程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> 關於JAVA >> 深刻解析java HashMap完成道理

深刻解析java HashMap完成道理

編輯:關於JAVA

深刻解析java HashMap完成道理。本站提示廣大學習愛好者:(深刻解析java HashMap完成道理)文章只能為提供參考,不一定能成為您想要的結果。以下是深刻解析java HashMap完成道理正文


Mark一下,同時可以很好的聯合hashCode()和equals()辦法,籠罩equals辦法時最好籠罩hashcode(),包管equals的兩個對象,hashcode也相等,反過去:hashcode()不等,必定能推出equals()也不等;hashcode()相等,equals()能夠相等,也能夠不等。

由於HashMap在get時,先比擬hashcode,再比擬equals,hashcode==&&equals,二者都為true,則以為是雷同的key

1.    HashMap概述:

   HashMap是基於哈希表的Map接口的非同步完成。此完成供給一切可選的映照操作,並許可應用null值和null鍵。此類不包管映照的次序,特殊是它不包管該次序長期不變。

2.    HashMap的數據構造:

   在java編程說話中,最根本的構造就是兩種,一個是數組,別的一個是模仿指針(援用),一切的數據構造都可以用這兩個根本構造來結構的,HashMap也不破例。HashMap現實上是一個“鏈表散列”的數據構造,即數組和鏈表的聯合體。

   從上圖中可以看出,HashMap底層就是一個數組構造,數組中的每項又是一個鏈表。當新建一個HashMap的時刻,就會初始化一個數組。

  源碼以下:

/** 
 * The table, resized as necessary. Length MUST Always be a power of two. 
 */ 
transient Entry[] table; 
static class Entry<K,V> implements Map.Entry<K,V> { 
  final K key; 
  V value; 
  Entry<K,V> next; 
  final int hash; 
  …… 
}

可以看出,Entry就是數組中的元素,每一個 Map.Entry 其實就是一個key-value對,它持有一個指向下一個元素的援用,這就組成了鏈表。

3.    HashMap的存取完成:

   1) 存儲:

public V put(K key, V value) { 
  // HashMap許可寄存null鍵和null值。 
  // 當key為null時,挪用putForNullKey辦法,將value放置在數組第一個地位。 
  if (key == null) 
    return putForNullKey(value); 
  // 依據key的keyCode從新盤算hash值。 
  int hash = hash(key.hashCode()); 
  // 搜刮指定hash值在對應table中的索引。 
  int i = indexFor(hash, table.length); 
  // 假如 i 索引處的 Entry 不為 null,經由過程輪回赓續遍歷 e 元素的下一個元素。 
  for (Entry<K,V> e = table[i]; e != null; e = e.next) { 
    Object k; 
    if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { 
      V oldValue = e.value; 
      e.value = value; 
      e.recordAccess(this); 
      return oldValue; 
    } 
  } 
  // 假如i索引處的Entry為null,注解此處還沒有Entry。 
  modCount++; 
  // 將key、value添加到i索引處。 
  addEntry(hash, key, value, i); 
  return null; 
} 

   從下面的源代碼中可以看出:當我們往HashMap中put元素的時刻,先依據key的hashCode從新盤算hash值,依據hash值獲得這個元素在數組中的地位(即下標),假如數組該地位上曾經寄存有其他元素了,那末在這個地位上的元素將以鏈表的情勢寄存,新參加的放在鏈頭,最早參加的放在鏈尾。假如數組該地位上沒有元素,就直接將該元素放到此數組中的該地位上。

addEntry(hash, key, value, i)辦法依據盤算出的hash值,將key-value對放在數組table的i索引處。addEntry 是 HashMap 供給的一個包拜訪權限的辦法,代碼以下:

void addEntry(int hash, K key, V value, int bucketIndex) { 
  // 獲得指定 bucketIndex 索引處的 Entry  
  Entry<K,V> e = table[bucketIndex]; 
  // 將新創立的 Entry 放入 bucketIndex 索引處,並讓新的 Entry 指向本來的 Entry 
  table[bucketIndex] = new Entry<K,V>(hash, key, value, e); 
  // 假如 Map 中的 key-value 對的數目跨越了極限 
  if (size++ >= threshold) 
  // 把 table 對象的長度擴大到本來的2倍。 
    resize(2 * table.length); 
} 

   當體系決議存儲HashMap中的key-value對時,完整沒有斟酌Entry中的value,僅僅只是依據key來盤算並決議每一個Entry的存儲地位。我們完整可以把 Map 聚集中的 value 當做 key 的從屬,當體系決議了 key 的存儲地位以後,value 隨之保留在那邊便可。

   hash(int h)辦法依據key的hashCode從新盤算一次散列。此算法參加了高位盤算,避免低位不變,高位變更時,形成的hash抵觸。

static int hash(int h) { 
  h ^= (h >>> 20) ^ (h >>> 12); 
  return h ^ (h >>> 7) ^ (h >>> 4); 
} 

   我們可以看到在HashMap中要找到某個元素,須要依據key的hash值來求得對應數組中的地位。若何盤算這個地位就是hash算法。後面說過HashMap的數據構造是數組和鏈表的聯合,所以我們固然願望這個HashMap外面的 元素地位盡可能的散布平均些,盡可能使得每一個地位上的元素數目只要一個,那末當我們用hash算法求得這個地位的時刻,立時便可以曉得對應地位的元素就是我們要的,而不消再去遍歷鏈表,如許就年夜年夜優化了查詢的效力。

   關於隨意率性給定的對象,只需它的 hashCode() 前往值雷同,那末法式挪用 hash(int h) 辦法所盤算獲得的 hash 碼值老是雷同的。我們起首想到的就是把hash值對數組長度取模運算,如許一來,元素的散布絕對來講是比擬平均的。然則,“模”運算的消費照樣比擬年夜的,在HashMap中是如許做的:挪用 indexFor(int h, int length) 辦法來盤算該對象應當保留在 table 數組的哪一個索引處。

indexFor(int h, int length) 辦法的代碼以下:

static int indexFor(int h, int length) { 
  return h & (length-1); 
} 

   這個辦法異常奇妙,它經由過程 h & (table.length -1) 來獲得該對象的保留位,而HashMap底層數組的長度老是 2 的 n 次方,這是HashMap在速度上的優化。在 HashMap 結構器中有以下代碼:

int capacity = 1; 
  while (capacity < initialCapacity) 
    capacity <<= 1; 

   這段代碼包管初始化時HashMap的容量老是2的n次方,即底層數組的長度老是為2的n次方。

當length老是 2 的n次方時,h& (length-1)運算等價於對length取模,也就是h%length,然則&比%具有更高的效力。

   這看上去很簡略,其實比擬有玄機的,我們舉個例子來講明:

   假定數組長度分離為15和16,優化後的hash碼分離為8和9,那末&運算後的成果以下:

       h & (table.length-1)                     hash                             table.length-1
       8 & (15-1):                                 0100                   &              1110                   =                0100
       9 & (15-1):                                 0101                   &              1110                   =                0100
       -----------------------------------------------------------------------------------------------------------------------
       8 & (16-1):                                 0100                   &              1111                   =                0100
       9 & (16-1):                                 0101                   &              1111                   =                0101

 從下面的例子中可以看出:當它們和15-1(1110)“與”的時刻,發生了雷同的成果,也就是說它們會定位到數組中的統一個地位上去,這就發生了碰撞,8和9會被放到數組中的統一個地位上構成鏈表,那末查詢的時刻就須要遍歷這個鏈 表,獲得8或許9,如許就下降了查詢的效力。同時,我們也能夠發明,當數組長度為15的時刻,hash值會與15-1(1110)停止“與”,那末 最初一名永久是0,而0001,0011,0101,1001,1011,0111,1101這幾個地位永久都不克不及寄存元素了,空間糟蹋相當年夜,更糟的是這類情形中,數組可使用的地位比數組長度小了許多,這意味著進一步增長了碰撞的概率,減慢了查詢的效力!而當數組長度為16時,即為2的n次方時,2n-1獲得的二進制數的每一個位上的值都為1,這使得在低位上&時,獲得的和原hash的低位雷同,加上hash(int h)辦法對key的hashCode的進一步優化,參加了高位盤算,就使得只要雷同的hash值的兩個值才會被放到數組中的統一個地位上構成鏈表。

   所以說,當數組長度為2的n次冪的時刻,分歧的key算得得index雷同的概率較小,那末數據在數組上散布就比擬平均,也就是說碰撞的概率小,絕對的,查詢的時刻就不消遍歷某個地位上的鏈表,如許查詢效力也就較高了。

   依據下面 put 辦法的源代碼可以看出,當法式試圖將一個key-value對放入HashMap中時,法式起首依據該 key 的 hashCode() 前往值決議該 Entry 的存儲地位:假如兩個 Entry 的 key 的 hashCode() 前往值雷同,那它們的存儲地位雷同。假如這兩個 Entry 的 key 經由過程 equals 比擬前往 true,新添加 Entry 的 value 將籠罩聚集華夏有 Entry 的 value,但key不會籠罩。假如這兩個 Entry 的 key 經由過程 equals 比擬前往 false,新添加的 Entry 將與聚集華夏有 Entry 構成 Entry 鏈,並且新添加的 Entry 位於 Entry 鏈的頭部——詳細解釋持續看 addEntry() 辦法的解釋。

   2) 讀取:

public V get(Object key) { 
  if (key == null) 
    return getForNullKey(); 
  int hash = hash(key.hashCode()); 
  for (Entry<K,V> e = table[indexFor(hash, table.length)]; 
    e != null; 
    e = e.next) { 
    Object k; 
    if (e.hash == hash && ((k = e.key) == key || key.equals(k))) 
      return e.value; 
  } 
  return null; 
}

   有了下面存儲時的hash算法作為基本,懂得起來這段代碼就很輕易了。從下面的源代碼中可以看出:從HashMap中get元素時,起首盤算key的hashCode,找到數組中對應地位的某一元素,然後經由過程key的equals辦法在對應地位的鏈表中找到須要的元素。

   3) 歸結起來簡略地說,HashMap 在底層將 key-value 當做一個全體停止處置,這個全體就是一個 Entry 對象。HashMap 底層采取一個 Entry[] 數組來保留一切的 key-value 對,當須要存儲一個 Entry 對象時,會依據hash算法來決議其在數組中的存儲地位,在依據equals辦法決議其在該數組地位上的鏈表中的存儲地位;當須要掏出一個Entry時,也會依據hash算法找到其在數組中的存儲地位,再依據equals辦法從該地位上的鏈表中掏出該Entry。

4.    HashMap的resize(rehash):

   當HashMap中的元素愈來愈多的時刻,hash抵觸的概率也就愈來愈高,由於數組的長度是固定的。所認為了進步查詢的效力,就要對HashMap的數組停止擴容,數組擴容這個操作也會湧現在ArrayList中,這是一個經常使用的操作,而在HashMap數組擴容以後,最消費機能的點就湧現了:原數組中的數據必需從新盤算其在新數組中的地位,並放出來,這就是resize。
   那末HashMap甚麼時刻停止擴容呢?當HashMap中的元素個數跨越數組年夜小*loadFactor時,就會停止數組擴容,loadFactor的默許值為0.75,這是一個折衷的取值。也就是說,默許情形下,數組年夜小為16,那末當HashMap中元素個數跨越16*0.75=12的時刻,就把數組的年夜小擴大為 2*16=32,即擴展一倍,然後從新盤算每一個元素在數組中的地位,而這是一個異常消費機能的操作,所以假如我們曾經預知HashMap中元素的個數,那末預設元素的個數可以或許有用的進步HashMap的機能。

5.    HashMap的機能參數:

   HashMap 包括以下幾個結構器:

   HashMap():構建一個初始容量為 16,負載因子為 0.75 的 HashMap。

   HashMap(int initialCapacity):構建一個初始容量為 initialCapacity,負載因子為 0.75 的 HashMap。

   HashMap(int initialCapacity, float loadFactor):以指定初始容量、指定的負載因子創立一個 HashMap。

   HashMap的基本結構器HashMap(int initialCapacity, float loadFactor)帶有兩個參數,它們是初始容量initialCapacity和加載因子loadFactor。

   initialCapacity:HashMap的最年夜容量,即為底層數組的長度。

   loadFactor:負載因子loadFactor界說為:散列表的現實元素數量(n)/ 散列表的容量(m)。

   負載因子權衡的是一個散列表的空間的應用水平,負載因子越年夜表現散列表的裝填水平越高,反之愈小。關於應用鏈表法的散列表來講,查找一個元素的均勻時光是O(1+a),是以假如負載因子越年夜,對空間的應用更充足,但是效果是查找效力的下降;假如負載因子太小,那末散列表的數據將過於稀少,對空間形成嚴重糟蹋。

   HashMap的完成中,經由過程threshold字段來斷定HashMap的最年夜容量:


threshold = (int)(capacity * loadFactor); 

   聯合負載因子的界說公式可知,threshold就是在此loadFactor和capacity對應下許可的最年夜元素數量,跨越這個數量就從新resize,以下降現實的負載因子。默許的的負載因子0.75是對空間和時光效力的一個均衡選擇。當容量超越此最年夜容量時, resize後的HashMap容量是容量的兩倍:

if (size++ >= threshold)   
  resize(2 * table.length);  

6.    Fail-Fast機制:

   我們曉得java.util.HashMap不是線程平安的,是以假如在應用迭代器的進程中有其他線程修正了map,那末將拋出ConcurrentModificationException,這就是所謂fail-fast戰略。
   這一戰略在源碼中的完成是經由過程modCount域,modCount望文生義就是修正次數,對HashMap內容的修正都將增長這個值,那末在迭代器初始化進程中會將這個值賦給迭代器的expectedModCount。

HashIterator() { 
  expectedModCount = modCount; 
  if (size > 0) { // advance to first entry 
  Entry[] t = table; 
  while (index < t.length && (next = t[index++]) == null) 
    ; 
  } 
}

   在迭代進程中,斷定modCount跟expectedModCount能否相等,假如不相等就表現曾經有其他線程修正了Map:
   留意到modCount聲明為volatile,包管線程之間修正的可見性。

final Entry<K,V> nextEntry() {   
  if (modCount != expectedModCount)   
    throw new ConcurrentModificationException(); 

   在HashMap的API中指出:

   由一切HashMap類的“collection 視圖辦法”所前往的迭代器都是疾速掉敗的:在迭代器創立以後,假如從構造上對映照停止修正,除非經由過程迭代器自己的 remove 辦法,其他任什麼時候間任何方法的修正,迭代器都將拋出 ConcurrentModificationException。是以,面臨並發的修正,迭代器很快就會完整掉敗,而不冒在未來不肯定的時光產生隨意率性不肯定行動的風險。

   留意,迭代器的疾速掉劣行為不克不及獲得包管,普通來講,存在非同步的並發修正時,弗成能作出任何果斷的包管。疾速掉敗迭代器盡最年夜盡力拋出 ConcurrentModificationException。是以,編寫依附於此異常的法式的做法是毛病的,准確做法是:迭代器的疾速掉劣行為應當僅用於檢測法式毛病。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved