国产片侵犯亲女视频播放_亚洲精品二区_在线免费国产视频_欧美精品一区二区三区在线_少妇久久久_在线观看av不卡

服務器之家:專注于服務器技術及軟件下載分享
分類導航

PHP教程|ASP.NET教程|Java教程|ASP教程|編程技術|正則表達式|C/C++|IOS|C#|Swift|Android|VB|R語言|JavaScript|易語言|vb.net|

服務器之家 - 編程語言 - Java教程 - 深入理解Java之HashMap源碼剖析

深入理解Java之HashMap源碼剖析

2020-08-12 10:46牛奶、不加糖 Java教程

這篇文章主要介紹了深入理解Java之HashMap源碼剖析,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧

一、HashMap概述

HashMap基于哈希表的 Map 接口的實現(xiàn)。此實現(xiàn)提供所有可選的映射操作,并允許使用 null 值和 null 鍵。(除了不同步和允許使用 null 之外,HashMap 類與 Hashtable 大致相同。)此類不保證映射的順序,特別是它不保證該順序恒久不變。

值得注意的是HashMap不是線程安全的,如果想要線程安全的HashMap,可以通過Collections類的靜態(tài)方法synchronizedMap獲得線程安全的HashMap。

?
1
Map map = Collections.synchronizedMap(new HashMap());

二、HashMap的數(shù)據(jù)結構

HashMap的底層主要是基于數(shù)組和鏈表來實現(xiàn)的,它之所以有相當快的查詢速度主要是因為它是通過計算散列碼來決定存儲的位置。HashMap中主要是通過key的hashCode來計算hash值的,只要hashCode相同,計算出來的hash值就一樣。如果存儲的對象對多了,就有可能不同的對象所算出來的hash值是相同的,這就出現(xiàn)了所謂的hash沖突。學過數(shù)據(jù)結構的同學都知道,解決hash沖突的方法有很多,HashMap底層是通過鏈表來解決hash沖突的。

深入理解Java之HashMap源碼剖析

 圖中,紫色部分即代表哈希表,也稱為哈希數(shù)組,數(shù)組的每個元素都是一個單鏈表的頭節(jié)點,鏈表是用來解決沖突的,如果不同的key映射到了數(shù)組的同一位置處,就將其放入單鏈表中。

我們看看HashMap中Entry類的代碼:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
/** Entry是單向鏈表。
 * 它是 “HashMap鏈式存儲法”對應的鏈表。
 *它實現(xiàn)了Map.Entry 接口,即實現(xiàn)getKey(), getValue(), setValue(V value), equals(Object o), hashCode()這些函數(shù)
**/
static class Entry<K,V> implements Map.Entry<K,V> {
 final K key;
 V value;
 // 指向下一個節(jié)點
 Entry<K,V> next;
 final int hash;
 
 // 構造函數(shù)。
 // 輸入?yún)?shù)包括"哈希值(h)", "鍵(k)", "值(v)", "下一節(jié)點(n)"
 Entry(int h, K k, V v, Entry<K,V> n) {
  value = v;
  next = n;
  key = k;
  hash = h;
 }
 
 public final K getKey() {
  return key;
 }
 
 public final V getValue() {
  return value;
 }
 
 public final V setValue(V newValue) {
  V oldValue = value;
  value = newValue;
  return oldValue;
 }
 
 // 判斷兩個Entry是否相等
 // 若兩個Entry的“key”和“value”都相等,則返回true。
 // 否則,返回false
 public final boolean equals(Object o) {
  if (!(o instanceof Map.Entry))
   return false;
  Map.Entry e = (Map.Entry)o;
  Object k1 = getKey();
  Object k2 = e.getKey();
  if (k1 == k2 || (k1 != null && k1.equals(k2))) {
   Object v1 = getValue();
   Object v2 = e.getValue();
   if (v1 == v2 || (v1 != null && v1.equals(v2)))
    return true;
  }
  return false;
 }
 
 // 實現(xiàn)hashCode()
 public final int hashCode() {
  return (key==null ? 0 : key.hashCode()) ^
    (value==null ? 0 : value.hashCode());
 }
 
 public final String toString() {
  return getKey() + "=" + getValue();
 }
 
 // 當向HashMap中添加元素時,繪調用recordAccess()。
 // 這里不做任何處理
 void recordAccess(HashMap<K,V> m) {
 }
 
 // 當從HashMap中刪除元素時,繪調用recordRemoval()。
 // 這里不做任何處理
 void recordRemoval(HashMap<K,V> m) {
 }
}

HashMap其實就是一個Entry數(shù)組,Entry對象中包含了鍵和值,其中next也是一個Entry對象,它就是用來處理hash沖突的,形成一個鏈表。

三、HashMap源碼分析

1、關鍵屬性

先看看HashMap類中的一些關鍵屬性:

?
1
2
3
4
5
6
7
8
9
transient Entry[] table;//存儲元素的實體數(shù)組
 
transient int size;//存放元素的個數(shù)
 
int threshold; //臨界值 當實際大小超過臨界值時,會進行擴容threshold = 加載因子*容量
 
 final float loadFactor; //加載因子
 
transient int modCount;//被修改的次數(shù)

其中l(wèi)oadFactor加載因子是表示Hsah表中元素的填滿的程度.

若:加載因子越大,填滿的元素越多,好處是,空間利用率高了,但:沖突的機會加大了.鏈表長度會越來越長,查找效率降低。

反之,加載因子越小,填滿的元素越少,好處是:沖突的機會減小了,但:空間浪費多了.表中的數(shù)據(jù)將過于稀疏(很多空間還沒用,就開始擴容了)

沖突的機會越大,則查找的成本越高.

因此,必須在 "沖突的機會"與"空間利用率"之間尋找一種平衡與折衷. 這種平衡與折衷本質上是數(shù)據(jù)結構中有名的"時-空"矛盾的平衡與折衷.

如果機器內存足夠,并且想要提高查詢速度的話可以將加載因子設置小一點;相反如果機器內存緊張,并且對查詢速度沒有什么要求的話可以將加載因子設置大一點。不過一般我們都不用去設置它,讓它取默認值0.75就好了。

 2、構造方法

下面看看HashMap的幾個構造方法:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public HashMap(int initialCapacity, float loadFactor) {
  //確保數(shù)字合法
  if (initialCapacity < 0)
   throw new IllegalArgumentException("Illegal initial capacity: " +
            initialCapacity);
  if (initialCapacity > MAXIMUM_CAPACITY)
   initialCapacity = MAXIMUM_CAPACITY;
  if (loadFactor <= 0 || Float.isNaN(loadFactor))
   throw new IllegalArgumentException("Illegal load factor: " +
            loadFactor);
 
  // Find a power of 2 >= initialCapacity
  int capacity = 1; //初始容量
  while (capacity < initialCapacity) //確保容量為2的n次冪,使capacity為大于initialCapacity的最小的2的n次冪
   capacity <<= 1;
 
  this.loadFactor = loadFactor;
  threshold = (int)(capacity * loadFactor);
  table = new Entry[capacity];
  init();
 }
 
 public HashMap(int initialCapacity) {
  this(initialCapacity, DEFAULT_LOAD_FACTOR);
 }
 
 public HashMap() {
  this.loadFactor = DEFAULT_LOAD_FACTOR;
  threshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR);
  table = new Entry[DEFAULT_INITIAL_CAPACITY];
  init();
 }

 我們可以看到在構造HashMap的時候如果我們指定了加載因子和初始容量的話就調用第一個構造方法,否則的話就是用默認的。默認初始容量為16,默認加載因子為0.75。我們可以看到上面代碼中13-15行,這段代碼的作用是確保容量為2的n次冪,使capacity為大于initialCapacity的最小的2的n次冪,至于為什么要把容量設置為2的n次冪,我們等下再看。

 重點分析下HashMap中用的最多的兩個方法put和get

 3、存儲數(shù)據(jù)

下面看看HashMap存儲數(shù)據(jù)的過程是怎樣的,首先看看HashMap的put方法:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public V put(K key, V value) {
  // 若“key為null”,則將該鍵值對添加到table[0]中。
   if (key == null)
   return putForNullKey(value);
  // 若“key不為null”,則計算該key的哈希值,然后將其添加到該哈希值對應的鏈表中。
   int hash = hash(key.hashCode());
  //搜索指定hash值在對應table中的索引
   int i = indexFor(hash, table.length);
  // 循環(huán)遍歷Entry數(shù)組,若“該key”對應的鍵值對已經(jīng)存在,則用新的value取代舊的value。然后退出!
   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))) { //如果key相同則覆蓋并返回舊值
     V oldValue = e.value;
     e.value = value;
     e.recordAccess(this);
     return oldValue;
    }
   }
  //修改次數(shù)+1
   modCount++;
  //將key-value添加到table[i]處
  addEntry(hash, key, value, i);
  return null;
}

上面程序中用到了一個重要的內部接口:Map.Entry,每個 Map.Entry 其實就是一個 key-value 對。從上面程序中可以看出:當系統(tǒng)決定存儲 HashMap 中的 key-value 對時,完全沒有考慮 Entry 中的 value,僅僅只是根據(jù) key 來計算并決定每個 Entry 的存儲位置。這也說明了前面的結論:我們完全可以把 Map 集合中的 value 當成 key 的附屬,當系統(tǒng)決定了 key 的存儲位置之后,value 隨之保存在那里即可。

我們慢慢的來分析這個函數(shù),第2和3行的作用就是處理key值為null的情況,我們看看putForNullKey(value)方法:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
private V putForNullKey(V value) {
  for (Entry<K,V> e = table[0]; e != null; e = e.next) {
   if (e.key == null) { //如果有key為null的對象存在,則覆蓋掉
    V oldValue = e.value;
    e.value = value;
    e.recordAccess(this);
    return oldValue;
   }
  }
  modCount++;
  addEntry(0, null, value, 0); //如果鍵為null的話,則hash值為0
  return null;
 }

注意:如果key為null的話,hash值為0,對象存儲在數(shù)組中索引為0的位置。即table[0]

我們再回去看看put方法中第4行,它是通過key的hashCode值計算hash碼,下面是計算hash碼的函數(shù):

?
1
2
3
4
5
6
7
8
//計算hash值的方法 通過鍵的hashCode來計算
 static int hash(int h) {
  // This function ensures that hashCodes that differ only by
  // constant multiples at each bit position have a bounded
  // number of collisions (approximately 8 at default load factor).
  h ^= (h >>> 20) ^ (h >>> 12);
  return h ^ (h >>> 7) ^ (h >>> 4);
 }

得到hash碼之后就會通過hash碼去計算出應該存儲在數(shù)組中的索引,計算索引的函數(shù)如下:

?
1
2
3
static int indexFor(int h, int length) { //根據(jù)hash值和數(shù)組長度算出索引值
 return h & (length-1); //這里不能隨便算取,用hash&(length-1)是有原因的,這樣可以確保算出來的索引是在數(shù)組大小范圍內,不會超出
}

這個我們要重點說下,我們一般對哈希表的散列很自然地會想到用hash值對length取模(即除法散列法),Hashtable中也是這樣實現(xiàn)的,這種方法基本能保證元素在哈希表中散列的比較均勻,但取模會用到除法運算,效率很低,HashMap中則通過h&(length-1)的方法來代替取模,同樣實現(xiàn)了均勻的散列,但效率要高很多,這也是HashMap對Hashtable的一個改進。

接下來,我們分析下為什么哈希表的容量一定要是2的整數(shù)次冪。首先,length為2的整數(shù)次冪的話,h&(length-1)就相當于對length取模,這樣便保證了散列的均勻,同時也提升了效率;其次,length為2的整數(shù)次冪的話,為偶數(shù),這樣length-1為奇數(shù),奇數(shù)的最后一位是1,這樣便保證了h&(length-1)的最后一位可能為0,也可能為1(這取決于h的值),即與后的結果可能為偶數(shù),也可能為奇數(shù),這樣便可以保證散列的均勻性,而如果length為奇數(shù)的話,很明顯length-1為偶數(shù),它的最后一位是0,這樣h&(length-1)的最后一位肯定為0,即只能為偶數(shù),這樣任何hash值都只會被散列到數(shù)組的偶數(shù)下標位置上,這便浪費了近一半的空間,因此,length取2的整數(shù)次冪,是為了使不同hash值發(fā)生碰撞的概率較小,這樣就能使元素在哈希表中均勻地散列。

這看上去很簡單,其實比較有玄機的,我們舉個例子來說明:

假設數(shù)組長度分別為15和16,優(yōu)化后的hash碼分別為8和9,那么&運算后的結果如下:

?
1
2
3
4
5
6
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)“與”的時候,產生了相同的結果,也就是說它們會定位到數(shù)組中的同一個位置上去,這就產生了碰撞,8和9會被放到數(shù)組中的同一個位置上形成鏈表,那么查詢的時候就需要遍歷這個鏈 表,得到8或者9,這樣就降低了查詢的效率。同時,我們也可以發(fā)現(xiàn),當數(shù)組長度為15的時候,hash值會與15-1(1110)進行“與”,那么 最后一位永遠是0,而0001,0011,0101,1001,1011,0111,1101這幾個位置永遠都不能存放元素了,空間浪費相當大,更糟的是這種情況中,數(shù)組可以使用的位置比數(shù)組長度小了很多,這意味著進一步增加了碰撞的幾率,減慢了查詢的效率!而當數(shù)組長度為16時,即為2的n次方時,2n-1得到的二進制數(shù)的每個位上的值都為1,這使得在低位上&時,得到的和原h(huán)ash的低位相同,加之hash(int h)方法對key的hashCode的進一步優(yōu)化,加入了高位計算,就使得只有相同的hash值的兩個值才會被放到數(shù)組中的同一個位置上形成鏈表。

所以說,當數(shù)組長度為2的n次冪的時候,不同的key算得得index相同的幾率較小,那么數(shù)據(jù)在數(shù)組上分布就比較均勻,也就是說碰撞的幾率小,相對的,查詢的時候就不用遍歷某個位置上的鏈表,這樣查詢效率也就較高了。

根據(jù)上面 put 方法的源代碼可以看出,當程序試圖將一個key-value對放入HashMap中時,程序首先根據(jù)該 key 的 hashCode() 返回值決定該 Entry 的存儲位置:如果兩個 Entry 的 key 的 hashCode() 返回值相同,那它們的存儲位置相同。如果這兩個 Entry 的 key 通過 equals 比較返回 true,新添加 Entry 的 value 將覆蓋集合中原有 Entry 的 value,但key不會覆蓋。如果這兩個 Entry 的 key 通過 equals 比較返回 false,新添加的 Entry 將與集合中原有 Entry 形成 Entry 鏈,而且新添加的 Entry 位于 Entry 鏈的頭部——具體說明繼續(xù)看 addEntry() 方法的說明。

?
1
2
3
4
5
6
void addEntry(int hash, K key, V value, int bucketIndex) {
  Entry<K,V> e = table[bucketIndex]; //如果要加入的位置有值,將該位置原先的值設置為新entry的next,也就是新entry鏈表的下一個節(jié)點
  table[bucketIndex] = new Entry<>(hash, key, value, e);
  if (size++ >= threshold) //如果大于臨界值就擴容
  resize(2 * table.length); //以2的倍數(shù)擴容
 }

 參數(shù)bucketIndex就是indexFor函數(shù)計算出來的索引值,第2行代碼是取得數(shù)組中索引為bucketIndex的Entry對象,第3行就是用hash、key、value構建一個新的Entry對象放到索引為bucketIndex的位置,并且將該位置原先的對象設置為新對象的next構成鏈表。

第4行和第5行就是判斷put后size是否達到了臨界值threshold,如果達到了臨界值就要進行擴容,HashMap擴容是擴為原來的兩倍。

 4、調整大小

resize()方法如下:

 重新調整HashMap的大小,newCapacity是調整后的單位

?
1
2
3
4
5
6
7
8
9
10
11
12
13
void resize(int newCapacity) {
  Entry[] oldTable = table;
  int oldCapacity = oldTable.length;
  if (oldCapacity == MAXIMUM_CAPACITY) {
   threshold = Integer.MAX_VALUE;
   return;
  }
 
  Entry[] newTable = new Entry[newCapacity];
  transfer(newTable);//用來將原先table的元素全部移到newTable里面
  table = newTable; //再將newTable賦值給table
  threshold = (int)(newCapacity * loadFactor);//重新計算臨界值
 }

新建了一個HashMap的底層數(shù)組,上面代碼中第10行為調用transfer方法,將HashMap的全部元素添加到新的HashMap中,并重新計算元素在新的數(shù)組中的索引位置

當HashMap中的元素越來越多的時候,hash沖突的幾率也就越來越高,因為數(shù)組的長度是固定的。所以為了提高查詢的效率,就要對HashMap的數(shù)組進行擴容,數(shù)組擴容這個操作也會出現(xiàn)在ArrayList中,這是一個常用的操作,而在HashMap數(shù)組擴容之后,最消耗性能的點就出現(xiàn)了:原數(shù)組中的數(shù)據(jù)必須重新計算其在新數(shù)組中的位置,并放進去,這就是resize。

那么HashMap什么時候進行擴容呢?當HashMap中的元素個數(shù)超過數(shù)組大小*loadFactor時,就會進行數(shù)組擴容,loadFactor的默認值為0.75,這是一個折中的取值。也就是說,默認情況下,數(shù)組大小為16,那么當HashMap中元素個數(shù)超過16*0.75=12的時候,就把數(shù)組的大小擴展為 2*16=32,即擴大一倍,然后重新計算每個元素在數(shù)組中的位置,擴容是需要進行數(shù)組復制的,復制數(shù)組是非常消耗性能的操作,所以如果我們已經(jīng)預知HashMap中元素的個數(shù),那么預設元素的個數(shù)能夠有效的提高HashMap的性能。

 5、數(shù)據(jù)讀取

?
1
2
3
4
5
6
7
8
9
10
11
12
13
public V get(Object key) {
 if (key == null)
  return getForNullKey();
 int hash = hash(keyhashCode());
 for (Entry<K,V> e = table[indexFor(hash, tablelength)];
  e != null;
  e = enext) {
  Object k;
  if (ehash == hash && ((k = ekey) == key || keyequals(k)))
   return evalue;
 }
 return null;
}

有了上面存儲時的hash算法作為基礎,理解起來這段代碼就很容易了。從上面的源代碼中可以看出:從HashMap中get元素時,首先計算key的hashCode,找到數(shù)組中對應位置的某一元素,然后通過key的equals方法在對應位置的鏈表中找到需要的元素。

6、HashMap的性能參數(shù): 

HashMap 包含如下幾個構造器:

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

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

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

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

 initialCapacity:HashMap的最大容量,即為底層數(shù)組的長度。

loadFactor:負載因子loadFactor定義為:散列表的實際元素數(shù)目(n)/ 散列表的容量(m)。

負載因子衡量的是一個散列表的空間的使用程度,負載因子越大表示散列表的裝填程度越高,反之愈小。對于使用鏈表法的散列表來說,查找一個元素的平均時間是O(1+a),因此如果負載因子越大,對空間的利用更充分,然而后果是查找效率的降低;如果負載因子太小,那么散列表的數(shù)據(jù)將過于稀疏,對空間造成嚴重浪費。

HashMap的實現(xiàn)中,通過threshold字段來判斷HashMap的最大容量:

?
1
threshold = (int)(capacity * loadFactor);

結合負載因子的定義公式可知,threshold就是在此loadFactor和capacity對應下允許的最大元素數(shù)目,超過這個數(shù)目就重新resize,以降低實際的負載因子。默認的的負載因子0.75是對空間和時間效率的一個平衡選擇。當容量超出此最大容量時, resize后的HashMap容量是容量的兩倍。

以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支持服務器之家。

原文鏈接:http://www.cnblogs.com/ITtangtang/p/3948406.html

延伸 · 閱讀

精彩推薦
Weibo Article 1 Weibo Article 2 Weibo Article 3 Weibo Article 4 Weibo Article 5 Weibo Article 6 Weibo Article 7 Weibo Article 8 Weibo Article 9 Weibo Article 10 Weibo Article 11 Weibo Article 12 Weibo Article 13 Weibo Article 14 Weibo Article 15 Weibo Article 16 Weibo Article 17 Weibo Article 18 Weibo Article 19 Weibo Article 20 Weibo Article 21 Weibo Article 22 Weibo Article 23 Weibo Article 24 Weibo Article 25 Weibo Article 26 Weibo Article 27 Weibo Article 28 Weibo Article 29 Weibo Article 30 Weibo Article 31 Weibo Article 32 Weibo Article 33 Weibo Article 34 Weibo Article 35 Weibo Article 36 Weibo Article 37 Weibo Article 38 Weibo Article 39 Weibo Article 40
主站蜘蛛池模板: 激情总合网| 亚洲怡红院在线观看 | 希岛爱理一区二区三区av高清 | 精品国产一区二区三区性色av | 亚洲视频自拍 | 成人欧美一区二区 | а√天堂中文在线资源8 | 中文精品一区二区 | 欧美性生活片 | 国产精品极品美女在线观看免费 | 国产精品99一区二区三区 | 国产精品亚洲视频 | 精品2区| 亚欧毛片| 精品国产91亚洲一区二区三区www | 久久99操| 亚洲精品高潮呻吟久久av | 日本中文字幕一区二区 | 日本不卡免费新一二三区 | 国产伦精品一区二区三区高清 | 欧美大片aaaa在线观看 | 国产精品a久久久久 | 国产黄视频在线 | 一区二区三区在线播放 | 国产一区二区三区在线 | 日韩欧美二区 | 黄色网址免费观看 | 操操操操操操 | 中文字幕在线视频一区 | 欧美成人不卡 | 亚洲国产精品久久久久久6q | 97人人爱 | 人人爱人人爽 | 一级毛片免费 | 在线观看中文字幕av | 国产精品亚洲一区二区三区在线 | 欧美一区亚洲一区 | 亚洲视频中文字幕 | 毛片免费观看视频 | 日韩在线| 久久久成人av |