HashMap

HashMap

初識HashMaphtml

以前的List,講了ArrayList、LinkedList,最後講到了CopyOnWriteArrayList,就前二者而言,反映的是兩種思想:面試

(1)ArrayList以數組形式實現,順序插入、查找快,插入、刪除較慢算法

(2)LinkedList以鏈表形式實現,順序插入、查找較慢,插入、刪除方便數組

那麼是否有一種數據結構可以結合上面兩種的優勢呢?有,答案就是HashMap。安全

HashMap是一種很是常見、方便和有用的集合,是一種鍵值對(K-V)形式的存儲結構,下面將仍是用圖示的方式解讀HashMap的實現原理,數據結構

 

四個關注點在HashMap上的答案app

關 注 點 結 論
HashMap是否容許空 Key和Value都容許爲空
HashMap是否容許重複數據 Key重複會覆蓋、Value容許重複
HashMap是否有序 無序,特別說明這個無序指的是遍歷HashMap的時候,獲得的元素的順序基本不多是put的順序
HashMap是否線程安全 非線程安全

先看下hashMap的基本組成:函數

static class Entry<K,V> implements Map.Entry<K,V> {
   final K key;
   V value;
   Entry<K,V> next;
   int hash;
  ...
}

圖示:性能

 

 

從HashMap的Entry看得出,Entry組成的是一個單向鏈表,由於裏面只有Entry的後繼Entry,而沒有Entry的前驅Entry,在new一個HashMap的時候,默認建立一個大小爲16的數組(DEFAULT_INITIAL_CAPACITY爲16),如圖:this

 

添加數據

先來一段代碼:

1 public static void main(String[] args)
2 {
3     Map<String, String> map = new HashMap<String, String>();
4     map.put("111", "111");
5     map.put("222", "222");
6 }

看下底層發生了什麼:

1 public V put(K key, V value) {
2     if (key == null)
3         return putForNullKey(value);
4     int hash = hash(key.hashCode());
5     int i = indexFor(hash, table.length);
6     for (Entry<K,V> e = table[i]; e != null; e = e.next) {
7        Object k;
8         if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
9             V oldValue = e.value;
10             e.value = value;
11             e.recordAccess(this);
12             return oldValue;
13         }
14     }
15
16     modCount++;
17     addEntry(hash, key, value, i);
18     return null;
19 }

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

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

一、第2行~第3行就是HashMap容許Key值爲空的緣由,空的Key會默認放在第0位的數組位置上

二、第4行拿到Key值的HashCode,因爲HashCode是Object的方法,所以每一個對象都有一個HashCode,對這個HashCode作一次hash計算。按照JDK源碼註釋的說法,此次hash的做用是根據給定的HashCode對它作一次打亂的操做,防止一些糟糕的Hash算法產生的糟糕的Hash值,至於爲何要防止糟糕的Hash值,HashMap添加元素的最後會講到

三、第5行根據從新計算的HashCode,對Entry數組的大小取模獲得一個Entry數組的位置。看到這裏使用了&,移位加快一點代碼運行效率。另外,這個取模操做的正確性依賴於length必須是2的N次冪,這個熟悉二進制的朋友必定理解,所以注意HashMap構造函數中,若是你指定HashMap初始數組的大小initialCapacity,若是initialCapacity不是2的N次冪,HashMap會算出大於initialCapacity的最小2的N次冪的值,做爲Entry數組的初始化大小。這裏爲了講解方便,咱們假定字符串111和字符串222算出來的i都是1

四、第6行~第14行會先判斷一下原數據結構中是否存在相同的Key值,存在則覆蓋並返回,不執行後面的代碼。注意一下recordAccess這個方法,它也是HashMap的子類好比LinkedHashMap用的,HashMap中這個方法爲空。另外,注意一點,對比Key是否相同,是先比HashCode是否相同,HashCode相同再判斷equals是否爲true,這樣大大增長了HashMap的效率,對HashCode不熟悉的朋友能夠看一下個人這篇文章講講HashCode的做用

五、第16行的modeCount++是用於fail-fast機制的,每次修改HashMap數據結構的時候都會自增一次這個值

圖示:

 

擴容

void addEntry(int hash, K key, V value, int bucketIndex) {
Entry<K,V> e = table[bucketIndex];
   table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
   if (size++ >= threshold)
       resize(2 * table.length);
}
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;
   threshold = (int)(newCapacity * loadFactor);
}
void transfer(Entry[] newTable) {
   Entry[] src = table;
   int newCapacity = newTable.length;
   for (int j = 0; j < src.length; j++) {
       Entry<K,V> e = src[j];
       if (e != null) {
           src[j] = null;
           do {
               Entry<K,V> next = e.next;
               int i = indexFor(e.hash, newCapacity);
               e.next = newTable[i];
               newTable[i] = e;
               e = next;
          } while (e != null);
      }
  }
}

 

我總結一下這三段代碼,HashMap一次擴容的過程應該是:

一、取當前table的2倍做爲新table的大小

二、根據算出的新table的大小new出一個新的Entry數組來,名爲newTable

三、輪詢原table的每個位置,將每一個位置上鍊接的Entry,算出在新table上的位置,並以鏈表形式鏈接

四、原table上的全部Entry所有輪詢完畢以後,意味着原table上面的全部Entry已經移到了新的table上,HashMap中的table指向newTable

這樣就完成了一次擴容,用圖表示是這樣的:

 

刪除

1 public V remove(Object key) {
2     Entry<K,V> e = removeEntryForKey(key);
3     return (e == null ? null : e.value);
4 }

1 final Entry<K,V> removeEntryForKey(Object key) {
2     int hash = (key == null) ? 0 : hash(key.hashCode());
3     int i = indexFor(hash, table.length);
4     Entry<K,V> prev = table[i];
5     Entry<K,V> e = prev;
6
7     while (e != null) {
8         Entry<K,V> next = e.next;
9         Object k;
10         if (e.hash == hash &&
11             ((k = e.key) == key || (key != null && key.equals(k)))) {
12             modCount++;
13             size--;
14             if (prev == e)
15                 table[i] = next;
16             else
17                 prev.next = next;
18             e.recordRemoval(this);
19             return e;
20         }
21         prev = e;
22         e = next;
23     }
24
25     return e;
26 }

分析一下remove元素的時候作了幾步:

一、根據key的hash找到待刪除的鍵值對位於table的哪一個位置上

二、記錄一個prev表示待刪除的Entry的前一個位置Entry,e能夠認爲是當前位置

三、從table[i]開始遍歷鏈表,假如找到了匹配的Entry,要作一個判斷,這個Entry是否是table[i]:

(1)是的話,也就是第14行~第15行,table[i]就直接是table[i]的下一個節點,後面的都不須要動

(2)不是的話,也就是第16行~第17行,e的前一個Entry也就是prev,prev的next指向e的後一個節點,也就是next,這樣,e所表明的Entry就被踢出了,e的先後Entry就連起來了

remove("111")用圖表示就是:

整個過程只須要修改一個節點的next的值便可,很是方便。

修改數據

1 public V put(K key, V value) {
2     if (key == null)
3         return putForNullKey(value);
4     int hash = hash(key.hashCode());
5     int i = indexFor(hash, table.length);
   //進行循環
6     for (Entry<K,V> e = table[i]; e != null; e = e.next) {
7         Object k;
    //判斷key是否相同,相同則進行替換
8         if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
9             V oldValue = e.value;
10             e.value = value;
11             e.recordAccess(this);
12             return oldValue;
13         }
14     }
15     modCount++;
16     addEntry(hash, key, value, i);
17     return null;
18 }

 

再談HashCode的重要性

前面講到了,HashMap中對Key的HashCode要作一次rehash,防止一些糟糕的Hash算法生成的糟糕的HashCode,那麼爲何要防止糟糕的HashCode?

糟糕的HashCode意味着的是Hash衝突,即多個不一樣的Key可能獲得的是同一個HashCode,糟糕的Hash算法意味着的就是Hash衝突的機率增大,這意味着HashMap的性能將降低,表如今兩方面:

一、有10個Key,可能6個Key的HashCode都相同,另外四個Key所在的Entry均勻分佈在table的位置上,而某一個位置上卻鏈接了6個Entry。這就失去了HashMap的意義,HashMap這種數據結構性高性能的前提是,Entry均勻地分佈在table位置上,但如今確是1 1 1 1 6的分佈。因此,咱們要求HashCode有很強的隨機性,這樣就儘量地能夠保證了Entry分佈的隨機性,提高了HashMap的效率。

二、HashMap在一個某個table位置上遍歷鏈表的時候的代碼:

if (e.hash == hash && ((k = e.key) == key || key.equals(k)))

看到,因爲採用了"&&"運算符,所以先比較HashCode,HashCode都不相同就直接pass了,不會再進行equals比較了。HashCode由於是int值,比較速度很是快,而equals方法每每會對比一系列的內容,速度會慢一些。Hash衝突的機率大,意味着equals比較的次數勢必增多,必然下降了HashMap的效率了。

簡單來講,就是要使hashCode要不重複或減小重複,才能使Entry均勻的分佈在table上,因此須要好的hash算法

 

HashMap的table爲何是transient的

一個很是細節的地方:

transient Entry[] table;

看到table用了transient修飾,也就是說table裏面的內容全都不會被序列化,不知道你們有沒有想過這麼寫的緣由?

在我看來,這麼寫是很是必要的。由於HashMap是基於HashCode的,HashCode做爲Object的方法,是native的:

public native int hashCode();

這意味着的是:HashCode和底層實現相關,不一樣的虛擬機可能有不一樣的HashCode算法。再進一步說得明白些就是,可能同一個Key在虛擬機A上的HashCode=1,在虛擬機B上的HashCode=2,在虛擬機C上的HashCode=3。

這就有問題了,Java自誕生以來,就以跨平臺性做爲最大賣點,好了,若是table不被transient修飾,在虛擬機A上能夠用的程序到虛擬機B上能夠用的程序就不能用了,失去了跨平臺性,由於:

一、Key在虛擬機A上的HashCode=100,連在table[4]上

二、Key在虛擬機B上的HashCode=101,這樣,就去table[5]上找Key,明顯找不到

 

HashMap和Hashtable的區別

HashMap和Hashtable是一組類似的鍵值對集合,它們的區別也是面試常被問的問題之一,我這裏簡單總結一下HashMap和Hashtable的區別:

一、Hashtable是線程安全的,Hashtable全部對外提供的方法都使用了synchronized,也就是同步,而HashMap則是線程非安全的

二、Hashtable不容許空的value,空的value將致使空指針異常,而HashMap則無所謂,沒有這方面的限制

三、上面兩個缺點是最主要的區別,另一個區別可有可無,我只是提一下,就是兩個的rehash算法不一樣

相關文章
相關標籤/搜索