Java集合專題總結(1):HashMap 和 HashTable 源碼學習和麪試總結

2017年的秋招完全結束了,感受Java上面的最多見的集合相關的問題就是hash……系列和一些經常使用併發集合和隊列,堆等結合算法一塊兒考察,不徹底統計,本人經歷:前後百度、惟品會、58同城、新浪微博、趣分期、美團點評等都在一、2……面的時候被問過無數次,都問吐了&_&,其餘公司筆試的時候,但凡是有Java的題,都有集合相關考點,尤爲hash表……如今總結下。面試

  • 2016-12-15 更新:Java 8 對 HashMap 的改進
  • 2016-12-12 整理jdk 1.8以前的HashMap實現

 

2016-12-15 更新:Java 8 對 HashMap 的改進算法

若是說Java的hashmap是數組+鏈表,那麼JDK 8以後就是數組+鏈表+紅黑樹組成了hashmap。以前的實現機制和原理在下面12-12期整理過,此次只說下新加的紅黑樹機制。數組

在以前談過,若是hash算法很差,會使得hash表蛻化爲順序查找,即便負載因子和hash算法優化再多,也沒法避免出現鏈表過長的情景(這個概論雖然很低),因而在JDK1.8中,對hashmap作了優化,引入紅黑樹。具體原理就是當hash表中每一個桶附帶的鏈表長度默認超過8時,鏈表就轉換爲紅黑樹結構,提升HashMap的性能,由於紅黑樹的增刪改是O(logn),而不是O(n)。安全

紅黑樹的具體原理和實現之後再總結。數據結構

主要看put方法實現

    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }
put方法

封裝了一個final方法,裏面用到一個常量,具體用處看源碼:多線程

static final int TREEIFY_THRESHOLD = 8;

下面是具體源代碼註釋:併發

 1     final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
 2                    boolean evict) {
 3         Node<K,V>[] tab; Node<K,V> p; int n, i;
 4         if ((tab = table) == null || (n = tab.length) == 0) // 首先判斷hash表是不是空的,若是空,則resize擴容
 5             n = (tab = resize()).length;
 6         if ((p = tab[i = (n - 1) & hash]) == null) // 經過key計算獲得hash表下標,若是下標處爲null,就新建鏈表頭結點,在方法最後插入便可
 7             tab[i] = newNode(hash, key, value, null);
 8         else { // 若是下標處已經存在節點,則進入到這裏
 9             Node<K,V> e; K k;
10             if (p.hash == hash &&
11                 ((k = p.key) == key || (key != null && key.equals(k)))) // 先看hash表該處的頭結點是否和key同樣(hashcode和equals比較),同樣就更新
12                 e = p;
13             else if (p instanceof TreeNode) // hash表頭結點和key不同,則判斷節點是否是紅黑樹,是紅黑樹就按照紅黑樹處理
14                 e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
15             else { // 若是不是紅黑樹,則按照以前的hashmap原理處理
16                 for (int binCount = 0; ; ++binCount) { // 遍歷鏈表
17                     if ((e = p.next) == null) {
18                         p.next = newNode(hash, key, value, null);
19                         if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st (原jdk註釋) 顯然當鏈表長度大於等於7的時候,也就是說大於8的話,就轉化爲紅黑樹結構,針對紅黑樹進行插入(logn複雜度)
20                             treeifyBin(tab, hash);
21                         break;
22                     }
23                     if (e.hash == hash &&
24                         ((k = e.key) == key || (key != null && key.equals(k)))) 
25                         break;
26                     p = e;
27                 }
28             }
29             if (e != null) { // existing mapping for key
30                 V oldValue = e.value;
31                 if (!onlyIfAbsent || oldValue == null)
32                     e.value = value;
33                 afterNodeAccess(e);
34                 return oldValue;
35             }
36         }
37         ++modCount;
38         if (++size > threshold) // 若是超過容量,即擴容
39             resize();
40         afterNodeInsertion(evict);
41         return null;
42     }
View Code

resize是新的擴容方法,以前談過,擴容原理是使用新的(2倍舊長度)的數組代替,把舊數組的內容放到新數組,須要從新計算hash和計算hash表的位置,很是耗時,可是自從 JDK 1.8 對hashmap 引入了紅黑樹,它和以前的擴容方法有了改進。app

擴容方法的改進

 1     final Node<K,V>[] resize() {
 2         Node<K,V>[] oldTab = table;
 3         int oldCap = (oldTab == null) ? 0 : oldTab.length;
 4         int oldThr = threshold;
 5         int newCap, newThr = 0;
 6         if (oldCap > 0) {
 7             if (oldCap >= MAXIMUM_CAPACITY) {
 8                 threshold = Integer.MAX_VALUE;
 9                 return oldTab;
10             }
11             else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && 
12                      oldCap >= DEFAULT_INITIAL_CAPACITY) // 若是長度沒有超過最大值,則擴容爲2倍的關係
13                 newThr = oldThr << 1; // double threshold
14         }
15         else if (oldThr > 0) // initial capacity was placed in threshold
16             newCap = oldThr;
17         else {               // zero initial threshold signifies using defaults
18             newCap = DEFAULT_INITIAL_CAPACITY;
19             newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
20         }
21         if (newThr == 0) {
22             float ft = (float)newCap * loadFactor;
23             newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
24                       (int)ft : Integer.MAX_VALUE);
25         }
26         threshold = newThr;
27         @SuppressWarnings({"rawtypes","unchecked"})
28             Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
29         table = newTab;
30         if (oldTab != null) { // 進行新舊元素的轉移過程
31             for (int j = 0; j < oldCap; ++j) {
32                 Node<K,V> e;
33                 if ((e = oldTab[j]) != null) {
34                     oldTab[j] = null;
35                     if (e.next == null)
36                         newTab[e.hash & (newCap - 1)] = e;
37                     else if (e instanceof TreeNode) 
38                         ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
39                     else { // preserve order(原註釋) 若是不是紅黑樹的狀況這裏改進了,沒有rehash的過程,以下分別記錄鏈表的頭尾
40                         Node<K,V> loHead = null, loTail = null;
41                         Node<K,V> hiHead = null, hiTail = null;
42                         Node<K,V> next;
43                         do {
44                             next = e.next;
45                             if ((e.hash & oldCap) == 0) {
46                                 if (loTail == null)
47                                     loHead = e;
48                                 else
49                                     loTail.next = e;
50                                 loTail = e;
51                             }
52                             else {
53                                 if (hiTail == null)
54                                     hiHead = e;
55                                 else
56                                     hiTail.next = e;
57                                 hiTail = e;
58                             }
59                         } while ((e = next) != null);
60                         if (loTail != null) {
61                             loTail.next = null;
62                             newTab[j] = loHead;
63                         }
64                         if (hiTail != null) {
65                             hiTail.next = null;
66                             newTab[j + oldCap] = hiHead;
67                         }
68                     }
69                 }
70             }
71         }
72         return newTab;
73     }
View Code

由於有這樣一個特色:好比hash表的長度是16,那麼15對應二進制是:ide

0000 0000, 0000 0000, 0000 0000, 0000 1111 = 15函數

擴容以前有兩個key,分別是k1和k2:

k1的hash:

0000 0000, 0000 0000, 0000 0000, 0000 1111 = 15

k2的hash:

0000 0000, 0000 0000, 0000 0000, 0001 1111 = 15

hash值和15模獲得:

k1:0000 0000, 0000 0000, 0000 0000, 0000 1111 = 15

k2:0000 0000, 0000 0000, 0000 0000, 0000 1111 = 15

擴容以後表長對應爲32,則31二進制:

0000 0000, 0000 0000, 0000 0000, 0001 1111 = 31

從新hash以後獲得:

k1:0000 0000, 0000 0000, 0000 0000, 0000 1111 = 15

k2:0000 0000, 0000 0000, 0000 0000, 0001 1111 = 31 = 15 + 16

觀察發現:若是擴容後新增的位是0,那麼rehash索引不變,不然纔會改變,而且變爲原來的索引+舊hash表的長度,故咱們只需看原hash表長新增的bit是1仍是0,若是是0,索引不變,若是是1,索引變成原索引+舊錶長,根本不用像JDK 7 那樣rehash,省去了從新計算hash值的時間,並且新增的bit是0仍是1能夠認爲是隨機的,所以resize的過程,還能均勻的把以前的衝突節點分散。 

故JDK 8對HashMap的優化是很是到位的。

 

以下是以前整理的舊hash的實現機制和原理,並和jdk古老的hashtable作了比較。


 

2016-12-12 整理jdk 1.8以前的HashMap實現:

  • Java集合概述
  • HashMap介紹
  • HashMap源碼學習
  • 關於HashMap的幾個經典問題
  • HashTable介紹和源碼學習
  • HashMap 和 HashTable 比較

 

先上圖

Set和List接口是Collection接口的子接口,分別表明無序集合和有序集合,Queue是Java提供的隊列實現。

 

Map用於保存具備key-value映射關係的數據

Java 中有四種常見的Map實現——HashMap, TreeMap, Hashtable和LinkedHashMap:

  • HashMap就是一張hash表,鍵和值都沒有排序。
  • TreeMap以紅黑樹結構爲基礎,鍵值能夠設置按某種順序排列。
  • LinkedHashMap保存了插入時的順序。
  • Hashtable是同步的(而HashMap是不一樣步的)。因此若是在線程安全的環境下應該多使用HashMap,而不是Hashtable,由於Hashtable對同步有額外的開銷,不過JDK 5以後的版本可使用conncurrentHashMao代替HashTable。

本文重點總結HashMap,HashMap是基於哈希表實現的,每個元素是一個key-value對,其內部經過單鏈表解決衝突問題,容量不足(超過了閥值)時,一樣會自動增加。

HashMap是非線程安全的,只用於單線程環境下,多線程環境下能夠採用concurrent併發包下的concurrentHashMap。

HashMap 實現了Serializable接口,所以它支持序列化。

HashMap還實現了Cloneable接口,故能被克隆。

 

關於hashmap的用法,這裏就再也不贅述了,只說原理和一些注意點。

HashMap的存儲結構

紫色部分即表明哈希表自己(實際上是一個數組),數組的每一個元素都是一個單鏈表的頭節點,鏈表是用來解決hash地址衝突的,若是不一樣的key映射到了數組的同一位置處,就將其放入單鏈表中保存。

 

HashMap有四個構造方法,方法中有兩個很重要的參數:初始容量和加載因子

這兩個參數是影響HashMap性能的重要參數,其中容量表示哈希表中槽的數量(即哈希數組的長度),初始容量是建立哈希表時的容量(默認爲16),加載因子是哈希表當前key的數量和容量的比值,當哈希表中的條目數超出了加載因子與當前容量的乘積時,則要對該哈希表提早進行 resize 操做(即擴容)。若是加載因子越大,對空間的利用更充分,可是查找效率會下降(鏈表長度會愈來愈長);若是加載因子過小,那麼表中的數據將過於稀疏(不少空間還沒用,就開始擴容了),嚴重浪費。

JDK開發者規定的默認加載因子爲0.75,由於這是一個比較理想的值。另外,不管指定初始容量爲多少,構造方法都會將實際容量設爲不小於指定容量的2的冪次方,且最大值不能超過2的30次方。

 

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

 1     // 獲取key對應的value
 2     public V get(Object key) {
 3         if (key == null)
 4             return getForNullKey();
 5         // 獲取key的hash值
 6         int hash = hash(key.hashCode());
 7         // 在「該hash值對應的鏈表」上查找「鍵值等於key」的元素
 8         for (Entry<K, V> e = table[indexFor(hash, table.length)]; e != null; e = e.next) {
 9             Object k;
10             // 判斷key是否相同
11             if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
12                 return e.value;
13         }
14         // 沒找到則返回null
15         return null;
16     }
17 
18     // 獲取「key爲null」的元素的值,HashMap將「key爲null」的元素存儲在table[0]位置,但不必定是該鏈表的第一個位置!
20     private V getForNullKey() {
21         for (Entry<K, V> e = table[0]; e != null; e = e.next) {
22             if (e.key == null)
23                 return e.value;
24         }
25         return null;
26     }
get方法源碼

首先,若是key爲null,則直接從哈希表的第一個位置table[0]對應的鏈表上查找。記住,key爲null的鍵值對永遠都放在以table[0]爲頭結點的鏈表中,固然不必定是存放在頭結點table[0]中。若是key不爲null,則先求的key的hash值,根據hash值找到在table中的索引,在該索引對應的單鏈表中查找是否有鍵值對的key與目標key相等,有就返回對應的value,沒有則返回null。

 1     // 將「key-value」添加到HashMap中
 2     public V put(K key, V value) {
 3         // 若「key爲null」,則將該鍵值對添加到table[0]中。
 4         if (key == null)
 5             return putForNullKey(value);
 6         // 若「key不爲null」,則計算該key的哈希值,而後將其添加到該哈希值對應的鏈表中。
 7         int hash = hash(key.hashCode());
 8         int i = indexFor(hash, table.length);
 9         for (Entry<K, V> e = table[i]; e != null; e = e.next) {
10             Object k;
11             // 若「該key」對應的鍵值對已經存在,則用新的value取代舊的value。而後退出!
12             if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
13                 V oldValue = e.value;
14                 e.value = value;
15                 e.recordAccess(this);
16                 return oldValue;
17             }
18         }
19 
20         // 若「該key」對應的鍵值對不存在,則將「key-value」添加到table中
21         modCount++;
22         // 將key-value添加到table[i]處
23         addEntry(hash, key, value, i);
24         return null;
25     }
put方法源碼

若是key爲null,則將其添加到table[0]對應的鏈表中,若是key不爲null,則一樣先求出key的hash值,根據hash值得出在table中的索引,然後遍歷對應的單鏈表,若是單鏈表中存在與目標key相等的鍵值對,則將新的value覆蓋舊的value,且將舊的value返回,若是找不到與目標key相等的鍵值對,或者該單鏈表爲空,則將該鍵值對插入到單鏈表的頭結點位置(每次新插入的節點都是放在頭結點的位置),該操做是有addEntry方法實現的,它的源碼以下:

    // 新增Entry。將「key-value」插入指定位置,bucketIndex是位置索引。
    void addEntry(int hash, K key, V value, int bucketIndex) {
        // 保存「bucketIndex」位置的值到「e」中
        Entry<K, V> e = table[bucketIndex];
        // 設置「bucketIndex」位置的元素爲「新Entry」,
        // 設置「e」爲「新Entry的下一個節點」
        table[bucketIndex] = new Entry<K, V>(hash, key, value, e);
        // 若HashMap的實際大小 不小於 「閾值」,則調整HashMap的大小
        if (size++ >= threshold)
            resize(2 * table.length);
    }
View Code

注意這裏倒數第三行的構造方法,將key-value鍵值對賦給table[bucketIndex],並將其next指向元素e,這便將key-value放到了頭結點中,並將以前的頭結點接在了它的後面。該方法也說明,每次put鍵值對的時候,老是將新的該鍵值對放在table[bucketIndex]處(即頭結點處)。兩外注意最後兩行代碼,每次加入鍵值對時,都要判斷當前已用的槽的數目是否大於等於閥值(容量*加載因子),若是大於等於,則進行擴容,將容量擴爲原來容量的2倍。

 

重點來分析下求hash值和索引值的方法,這兩個方法即是HashMap設計的最爲核心的部分,兩者結合能保證哈希表中的元素儘量均勻地散列。

由hash值找到對應索引的方法以下

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

由於容量初始仍是設定都會轉化爲2的冪次。故可使用高效的位與運算替代模運算。下面會解釋緣由。

 

計算hash值的方法以下

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

JDK 的 HashMap 使用了一個 hash 方法對hash值使用位的操做,使hash值的計算效率很高。爲何這樣作?主要是由於若是直接使用hashcode值,那麼這是一個int值(8個16進制數,共32位),int值的範圍正負21億多,可是hash表沒有那麼長,通常好比初始16,天然散列地址須要對hash表長度取模運算,獲得的餘數纔是地址下標。假設某個key的hashcode是0AAA0000,hash數組長默認16,若是不通過hash函數處理,該鍵值對會被存放在hash數組中下標爲0處,由於0AAA0000 & (16-1) = 0。過了一下子又存儲另一個鍵值對,其key的hashcode是0BBB0000,獲得數組下標依然是0,這就說明這是個實現得不好的hash算法,由於hashcode的1位全集中在前16位了,致使算出來的數組下標一直是0。因而明明key相差很大的鍵值對,卻存放在了同一個鏈表裏,致使之後查詢起來比較慢(蛻化爲了順序查找)。故JDK的設計者使用hash函數的若干次的移位、異或操做,把hashcode的「1位」變得「鬆散」,很是巧妙。

 

下面是幾個常見的面試題

說下hashmap的 擴容機制?

前面說了,hashmap的構造器裏指明瞭兩個對於理解HashMap比較重要的兩個參數 int initialCapacity, float loadFactor,這兩個參數會影響HashMap效率,HashMap底層採用的散列數組實現,利用initialCapacity這個參數咱們能夠設置這個數組的大小,也就是散列桶的數量,可是若是須要Map的數據過多,在不斷的add以後,這些桶可能都會被佔滿,這是有兩種策略,一種是不改變Capacity,由於即便桶佔滿了,咱們仍是能夠利用每一個桶附帶的鏈表增長元素。可是這有個缺點,此時HaspMap就退化成爲了LinkedList,使get和put方法的時間開銷上升,這是就要採用另外一種方法:增長Hash桶的數量,這樣get和put的時間開銷又回退到近於常數複雜度上。Hashmap就是採用的該方法。

關於擴容。看hashmap的擴容方法,resize方法,它的源碼以下

 1     // 從新調整HashMap的大小,newCapacity是調整後的單位
 2     void resize(int newCapacity) {
 3         Entry[] oldTable = table;
 4         int oldCapacity = oldTable.length;
 5         if (oldCapacity == MAXIMUM_CAPACITY) {
 6             threshold = Integer.MAX_VALUE;
 7             return;
 8         }
 9 
10         // 新建一個HashMap,將「舊HashMap」的所有元素添加到「新HashMap」中,
11         // 而後,將「新HashMap」賦值給「舊HashMap」。
12         Entry[] newTable = new Entry[newCapacity];
13         transfer(newTable);
14         table = newTable;
15         threshold = (int) (newCapacity * loadFactor);
16     }
擴容的resize方法

很明顯,是重新建了一個HashMap的底層數組,長度爲原來的兩倍,然後調用transfer方法,將舊HashMap的所有元素添加到新的HashMap中(要從新計算元素在新的數組中的索引位置)。transfer方法的源碼以下:

 1     // 將HashMap中的所有元素都添加到newTable中
 2     void transfer(Entry[] newTable) {
 3         Entry[] src = table;
 4         int newCapacity = newTable.length;
 5         for (int j = 0; j < src.length; j++) {
 6             Entry<K, V> e = src[j];
 7             if (e != null) {
 8                 src[j] = null;
 9                 do {
10                     Entry<K, V> next = e.next;
11                     int i = indexFor(e.hash, newCapacity);
12                     e.next = newTable[i];
13                     newTable[i] = e;
14                     e = next;
15                 } while (e != null);
16             }
17         }
18     }
transfer方法源碼

很明顯,擴容是一個至關耗時的操做,由於它須要從新計算這些元素在新的數組中的位置並進行復制處理。所以,咱們在用HashMap時,最好能提早預估下HashMap中元素的個數,這樣有助於提升HashMap的性能。

 

hashmap何時須要增長容量呢?

由於效率問題,JDK採用預處理法,這時前面說的loadFactor就派上了用場,當size > initialCapacity * loadFactor,hashmap內部resize方法就被調用,使得從新擴充hash桶的數量,在目前的實現中,是增長一倍,這樣就保證當你真正想put新的元素時效率不會明顯降低。因此通常狀況下HashMap並不存在鍵值放滿的狀況。固然並不排除極端狀況,好比設置的JVM內存用完了,或者這個HashMap的Capacity已經達到了MAXIMUM_CAPACITY(目前的實現是2^30)。

 

initialCapacity和loadFactor參數設什麼樣的值好呢?

initialCapacity的默認值是16,有些人可能會想若是內存足夠,是否是能夠將initialCapacity設大一些,即便用不了這麼大,就可避免擴容致使的效率的降低,反正不管initialCapacity大小,咱們使用的get和put方法都是常數複雜度的。這麼說沒什麼不對,可是可能會忽略一點,實際的程序可能不只僅使用get和put方法,也有可能使用迭代器,如initialCapacity容量較大,那麼會使迭代器效率下降。因此理想的狀況仍是在使用HashMap前估計一下數據量。

加載因子默認值是0.75,是JDK權衡時間和空間效率以後獲得的一個相對優良的數值。若是這個值過大,雖然空間利用率是高了,可是對於HashMap中的一些方法的效率就降低了,包括get和put方法,會致使每一個hash桶所附加的鏈表增加,影響存取效率。若是比較小,除了致使空間利用率較低外沒有什麼壞處,只要有的是內存,畢竟如今大多數人把時間看的比空間重要。可是實際中仍是不多有人會將這個值設置的低於0.5。

 

HashMap的key和value都能爲null麼?若是k能爲null,那麼它是怎麼樣查找值的?

若是key爲null,則直接從哈希表的第一個位置table[0]對應的鏈表上查找。記住,key爲null的鍵值對永遠都放在以table[0]爲頭結點的鏈表中。

 

HashMap中put值的時候若是發生了衝突,是怎麼處理的?

JDK使用了鏈地址法,hash表的每一個元素又分別連接着一個單鏈表,元素爲頭結點,若是不一樣的key映射到了相同的下標,那麼就使用頭插法,插入到該元素對應的鏈表。

 

HashMap的key是如何散列到hash表的?相比較HashTable有什麼改進?

咱們通常對哈希表的散列很天然地會想到用hash值對length取模(即除留餘數法),HashTable就是這樣實現的,這種方法基本能保證元素在哈希表中散列的比較均勻,但取模會用到除法運算,效率很低,且hashtable直接使用了hashcode值,沒有從新計算。

HashMap中則經過 h&(length-1) 的方法來代替取模,其中h是key的hash值,一樣實現了均勻的散列,但效率要高不少,這也是HashMap對Hashtable的一個改進。

接下來,咱們分析下爲何哈希表的容量必定要是2的整數次冪。

首先,length爲2的整數次冪的話,h&(length-1) 在數學上就至關於對length取模,這樣便保證了散列的均勻,同時也提高了效率;

其次,length爲2的整數次冪的話,則必定爲偶數,那麼 length-1 必定爲奇數,奇數的二進制的最後一位是1,這樣便保證了 h&(length-1) 的最後一位可能爲0,也可能爲1(這取決於h的值),即與後的結果可能爲偶數,也可能爲奇數,這樣即可以保證散列的均勻,而若是length爲奇數的話,很明顯 length-1 爲偶數,它的最後一位是0,這樣 h&(length-1) 的最後一位確定爲0,即只能爲偶數,這樣致使了任何hash值都只會被散列到數組的偶數下標位置上,浪費了一半的空間,所以length取2的整數次冪,是爲了使不一樣hash值發生碰撞的機率較小,這樣就能使元素在哈希表中均勻地散列。

 

做爲對比,在討論一下Hashtable

HashTable一樣是基於哈希表實現的,其實相似HashMap,只不過有些區別,HashTable一樣每一個元素是一個key-value對,其內部也是經過單鏈表解決衝突問題,容量不足(超過了閥值)時,一樣會自動增加。

HashTable比較古老, 是JDK1.0就引入的類,而HashMap 是 1.2 引進的 Map 的一個實現。

HashTable 是線程安全的,能用於多線程環境中。Hashtable一樣也實現了Serializable接口,支持序列化,也實現了Cloneable接口,能被克隆。

Hashtable繼承於Dictionary類,實現了Map接口。Dictionary是聲明瞭操做"鍵值對"函數接口的抽象類。 有一點注意,HashTable除了線程安全以外(實際上是直接在方法上增長了synchronized關鍵字,比較古老,落後,低效的同步方式),還有就是它的key、value都不爲null。另外Hashtable 也有 初始容量 和 加載因子

    public Hashtable() {
        this(11, 0.75f);
    }

默認加載因子也是 0.75,HashTable在不指定容量的狀況下的默認容量爲11,而HashMap爲16,Hashtable不要求底層數組的容量必定要爲2的整數次冪,而HashMap則要求必定爲2的整數次冪。由於HashTable是直接使用除留餘數法定位地址。且Hashtable計算hash值,直接用key的hashCode()。

還要注意:前面說了Hashtable中key和value都不容許爲null,而HashMap中key和value都容許爲null(key只能有一個爲null,而value則能夠有多個爲null)。但如在Hashtable中有相似put(null,null)的操做,編譯一樣能夠經過,由於key和value都是Object類型,但運行時會拋出NullPointerException異常,這是JDK的規範規定的。

最後針對擴容:Hashtable擴容時,將容量變爲原來的2倍加1,而HashMap擴容時,將容量變爲原來的2倍。

 

下面是幾個常見的筆試,面試題

HashTable和HashMap的區別有哪些?

HashMap和Hashtable都實現了Map接口,但決定用哪個以前先要弄清楚它們之間的分別。主要的區別有:線程安全性,同步(synchronization),以及速度。

理解HashMap是Hashtable的輕量級實現(非線程安全的實現,hashtable是非輕量級,線程安全的),都實現Map接口,主要區別在於:

一、因爲HashMap非線程安全,在只有一個線程訪問的狀況下,效率要高於HashTable

二、HashMap容許將null做爲一個entry的key或者value,而Hashtable不容許。

三、HashMap把Hashtable的contains方法去掉了,改爲containsValue和containsKey。由於contains方法容易讓人引發誤解。

四、Hashtable繼承自陳舊的Dictionary類,而HashMap是Java1.2引進的Map 的一個實現。

五、Hashtable和HashMap擴容的方法不同,HashTable中hash數組默認大小11,擴容方式是 old*2+1。HashMap中hash數組的默認大小是16,並且必定是2的指數,增長爲原來的2倍,沒有加1。

六、二者經過hash值散列到hash表的算法不同,HashTbale是古老的除留餘數法,直接使用hashcode,然後者是強制容量爲2的冪,從新根據hashcode計算hash值,在使用hash  位與  (hash表長度 – 1),也等價取膜,但更加高效,取得的位置更加分散,偶數,奇數保證了都會分散到。前者就不能保證。

七、另外一個區別是HashMap的迭代器(Iterator)是fail-fast迭代器,而Hashtable的enumerator迭代器不是fail-fast的。因此當有其它線程改變了HashMap的結構(增長或者移除元素),將會拋出ConcurrentModificationException,但迭代器自己的remove()方法移除元素則不會拋出ConcurrentModificationException異常。但這並非一個必定發生的行爲,要看JVM。這條一樣也是Enumeration和Iterator的區別。

  • fail-fast和iterator迭代器相關。若是某個集合對象建立了Iterator或者ListIterator,而後其它的線程試圖「結構上」更改集合對象,將會拋出ConcurrentModificationException異常。但其它線程能夠經過set()方法更改集合對象是容許的,由於這並無從「結構上」更改集合。可是假如已經從結構上進行了更改,再調用set()方法,將會拋出IllegalArgumentException異常。 
  • 結構上的更改指的是刪除或者插入一個元素,這樣會影響到map的結構。
  • 該條說白了就是在使用迭代器的過程當中有其餘線程在結構上修改了map,那麼將拋出ConcurrentModificationException,這就是所謂fail-fast策略。

 

爲何HashMap是線程不安全的,實際會如何體現?

第一,若是多個線程同時使用put方法添加元素

假設正好存在兩個put的key發生了碰撞(hash值同樣),那麼根據HashMap的實現,這兩個key會添加到數組的同一個位置,這樣最終就會發生其中一個線程的put的數據被覆蓋。

第二,若是多個線程同時檢測到元素個數超過數組大小*loadFactor

這樣會發生多個線程同時對hash數組進行擴容,都在從新計算元素位置以及複製數據,可是最終只有一個線程擴容後的數組會賦給table,也就是說其餘線程的都會丟失,而且各自線程put的數據也丟失。且會引發死循環的錯誤。

具體細節上的緣由,能夠參考:不正當使用HashMap致使cpu 100%的問題追究

 

可否讓HashMap實現線程安全,如何作?

一、直接使用Hashtable,可是當一個線程訪問HashTable的同步方法時,其餘線程若是也要訪問同步方法,會被阻塞住。舉個例子,當一個線程使用put方法時,另外一個線程不但不可使用put方法,連get方法都不能夠,效率很低,如今基本不會選擇它了。

二、HashMap能夠經過下面的語句進行同步:

Collections.synchronizeMap(hashMap);

三、直接使用JDK 5 以後的 ConcurrentHashMap,若是使用Java 5或以上的話,請使用ConcurrentHashMap。

 

Collections.synchronizeMap(hashMap);又是如何保證了HashMap線程安全?

直接分析源碼吧

 1 // synchronizedMap方法
 2 public static <K,V> Map<K,V> synchronizedMap(Map<K,V> m) {
 3        return new SynchronizedMap<>(m);
 4    }
 5 // SynchronizedMap類
 6 private static class SynchronizedMap<K,V>
 7        implements Map<K,V>, Serializable {
 8        private static final long serialVersionUID = 1978198479659022715L;
 9  
10        private final Map<K,V> m;     // Backing Map
11        final Object      mutex;        // Object on which to synchronize
12  
13        SynchronizedMap(Map<K,V> m) {
14            this.m = Objects.requireNonNull(m);
15            mutex = this;
16        }
17  
18        SynchronizedMap(Map<K,V> m, Object mutex) {
19            this.m = m;
20            this.mutex = mutex;
21        }
22  
23        public int size() {
24            synchronized (mutex) {return m.size();}
25        }
26        public boolean isEmpty() {
27            synchronized (mutex) {return m.isEmpty();}
28        }
29        public boolean containsKey(Object key) {
30            synchronized (mutex) {return m.containsKey(key);}
31        }
32        public boolean containsValue(Object value) {
33            synchronized (mutex) {return m.containsValue(value);}
34        }
35        public V get(Object key) {
36            synchronized (mutex) {return m.get(key);}
37        }
38  
39        public V put(K key, V value) {
40            synchronized (mutex) {return m.put(key, value);}
41        }
42        public V remove(Object key) {
43            synchronized (mutex) {return m.remove(key);}
44        }
45        // 省略其餘方法
46    }
View Code

從源碼中看出 synchronizedMap()方法返回一個SynchronizedMap類的對象,而在SynchronizedMap類中使用了synchronized來保證對Map的操做是線程安全的,故效率其實也不高。

 

爲何HashTable的默認大小和HashMap不同?

前面分析了,Hashtable 的擴容方法是乘2再+1,不是簡單的乘2,故hashtable保證了容量永遠是奇數,結合以前分析hashmap的重算hash值的邏輯,就明白了,由於在數據分佈在等差數據集合(如偶數)上時,若是公差與桶容量有公約數 n,則至少有(n-1)/n 數量的桶是利用不到的,故以前的hashmap 會在取模(使用位與運算代替)哈希前先作一次哈希運算,調整hash值。這裏hashtable比較古老,直接使用了除留餘數法,那麼就須要設置容量起碼不是偶數(除(近似)質數求餘的分散效果好)。而JDK開發者選了11。

 

JDK 8對HashMap有了什麼改進?說說你對紅黑樹的理解?

參考更新的jdk 8對hashmap的的改進部分整理,而且還能引伸出高級數據結構——紅黑樹,這又能引出不少問題……學無止境啊!

 

臨時小結:感受針對Java的hashmap和hashtable面試,或者理解,到這裏就能夠了,具體就是多寫代碼實踐。

相關文章
相關標籤/搜索