HashMap和Hashtable的主要區別是:
1. Hashtable是線程安全,而HashMap則非線程安全,Hashtable的實現方法裏面大部分都添加了synchronized關鍵字來確保線程同步,所以相對而言HashMap性能會高一些,在多線程環境下若使用HashMap須要使用Collections.synchronizedMap()方法來獲取一個線程安全的集合。java
2. HashMap的鍵和值均可覺得null,而Hashtable的鍵值都不能爲null。node
3. HashMap的初始容量爲16,Hashtable初始容量爲11,二者的填充因子默認都是0.75。HashMap擴展容量是當前容量翻倍即:capacity*2,Hashtable擴展容量是容量翻倍+1即:capacity*2+1(關於擴容和填充因子後面會講)算法
4. 二者的哈希算法不一樣,HashMap是先對key(鍵)求hashCode碼,而後再把這個碼值得高位和低位作異或運算,源碼以下:數組
static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); } i = (n - 1) & hash
而後把hash(key)返回的哈希值與HashMap的初始容量(也叫初始數組的長度)減一作&(與運算)就能夠計算出此鍵值對應該保存到數組的那個位置上(hash&(n-1))。安全
而Hashtable計算位置的方式以下:多線程
int hash = key.hashCode(); int index = (hash & 0x7FFFFFFF) % tab.length;
直接計算key的哈希碼,而後與2的31次方作&(與運算),而後對數組長度取餘數計算位置。app
hashMap的一些重要屬性:源碼分析
/默認容量 static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; //最大容量 static final int MAXIMUM_CAPACITY = 1 << 30; //默認加載因子 static final float DEFAULT_LOAD_FACTOR = 0.75f; //鏈表轉成紅黑樹的閾值 static final int TREEIFY_THRESHOLD = 8; //紅黑樹轉爲鏈表的閾值 static final int UNTREEIFY_THRESHOLD = 6; //存儲方式由鏈表轉成紅黑樹的容量的最小閾值 static final int MIN_TREEIFY_CAPACITY = 64; //HashMap中存儲的鍵值對的數量 transient int size; //擴容閾值,當size>=threshold時,就會擴容 int threshold; //HashMap的加載因子 final float loadFactor;
接下來是HashMap的put和remove源碼分析性能
HashMap結構就是Node數組,Node源碼:this
static class Node<K,V> implements Map.Entry<K,V> { final int hash; final K key; V value; Node<K,V> next; Node(int hash, K key, V value, Node<K,V> next) { this.hash = hash; this.key = key; this.value = value; this.next = next; } public final K getKey() { return key; } public final V getValue() { return value; } public final String toString() { return key + "=" + value; } public final int hashCode() { return Objects.hashCode(key) ^ Objects.hashCode(value); } public final V setValue(V newValue) { V oldValue = value; value = newValue; return oldValue; } public final boolean equals(Object o) { if (o == this) return true; if (o instanceof Map.Entry) { Map.Entry<?,?> e = (Map.Entry<?,?>)o; if (Objects.equals(key, e.getKey()) && Objects.equals(value, e.getValue())) return true; } return false; } }
put源碼分析:
final V putVal ( int hash, K key, V value,boolean onlyIfAbsent, boolean evict){ HashMap.Node<K, V>[] tab; HashMap.Node<K, V> p; int n, i; if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length;//第一次調用的時候,數組長度爲零,調用resize(),此時數組長度變爲16 if ((p = tab[i = (n - 1) & hash]) == null)//根據傳入的hash值,算出Node所在的數組標,拿出Node,若是爲空表示當前數組尚未鏈表,建立新的節點放在數組這個位置 tab[i] = newNode(hash, key, value, null); else {//若取出的節點p不爲空 HashMap.Node<K, V> e; K k; if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))//只有當取出的節點hash值與Key都與傳參同樣時才認爲兩個Node爲同一個 e = p;// e指向原有的Node else if (p instanceof HashMap.TreeNode)//若是找到的當前節點不是key對應的Node,且爲TreeNode,執行treeNode的put邏輯 e = ((HashMap.TreeNode<K, V>) p).putTreeVal(this, tab, hash, key, value); else { for (int binCount = 0; ; ++binCount) {// binCount 用來記錄當前鏈表長度 if ((e = p.next) == null) {//若是當前Node 不是咱們要找的,遍歷這個鏈表數據 p.next = newNode(hash, key, value, null);//若是節點的下一個節點爲空,建立新的節點,放在現有的節點後面 if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st,若是鏈表長度 >= 8 ,則將鏈表轉換爲紅黑樹 treeifyBin(tab, hash);//轉換爲紅黑樹 break; } if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))//若是當前節點的下一個節點就是咱們要找的元素,直接跳出for循環,e是最終要找的Node break; p = e; } } if (e != null) { // existing mapping for key,若是e不爲空,表示找到了對應的節點 V oldValue = e.value;//oldValue 用戶存儲老的值 if (!onlyIfAbsent || oldValue == null) e.value = value;//將新值覆蓋原有的值 afterNodeAccess(e); return oldValue; // 找到對應的節點之後,返回值爲舊value } } //如下代碼爲只有當e爲空,也就是沒有找到相匹配的key值得地方,這是就建立了一個新的Node,添加到某個鏈表後面 ++modCount;// modCount 用於記錄map 被修改了多少次 if (++size > threshold)// size 表示當前map 有多少個 Node,添加一個元素Node resize();//當添加了一個Node之後 size 若是大於 承載量 則進行擴容 afterNodeInsertion(evict); return null;//若是沒有找到你對應的節點,執行新增操做,返回null }
remove源碼分析:
final HashMap.Node<K, V> removeNode ( int hash, Object key, Object value, boolean matchValue, boolean movable){ HashMap.Node<K, V>[] tab; HashMap.Node<K, V> p; int n, index; if ((tab = table) != null && (n = tab.length) > 0 && (p = tab[index = (n - 1) & hash]) != null) {//根據hash值定位到要移除的元素位置 HashMap.Node<K, V> node = null, e; K k; V v; if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) node = p;//若是找到了對應的元素,讓臨時變量node指針指向被刪除元素 else if ((e = p.next) != null) {//若是當前節點沒有找到,則向下遍歷鏈表 if (p instanceof HashMap.TreeNode)//若是是TreeNode ,將節點強制轉換爲TreeNode node = ((HashMap.TreeNode<K, V>) p).getTreeNode(hash, key); else { do { if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) {//遍歷整個鏈表,若是找到要刪除的節點就將node指針指向該節點 node = e; break; } p = e; } while ((e = e.next) != null); } } //若是node不爲空,則表示找到了要刪除的節點 if (node != null && (!matchValue || (v = node.value) == value || (value != null && value.equals(v)))) {//該節點值與要刪的節點value一致,執行刪除動做 if (node instanceof HashMap.TreeNode)//若是當前節點是treeNode,則執行treeNode的刪除邏輯 ((HashMap.TreeNode<K, V>) node).removeTreeNode(this, tab, movable); else if (node == p)//若是當前節點就是要刪除的節點,則把節點的下一個節點放在數組位置,相似於把頭給掐斷 tab[index] = node.next; else//若是要刪除節點不在頭部的話,這種狀況下,p始終爲要刪除節點的前一個節點 p.next = node.next;//將p節點的next指針指向node的下一個節點,即將node節點刪除 ++modCount;//統計map被修改的次數 --size;//map的 Node 個數減一 afterNodeRemoval(node); return node;//返回被刪除的Node節點 } } return null;//若是沒找到被刪除節點 ,返回null }
擴容源碼分析:
final HashMap.Node<K,V>[] resize() { HashMap.Node<K,V>[] oldTab = table; int oldCap = (oldTab == null) ? 0 : oldTab.length;//舊的容量,即數組長度 int oldThr = threshold;//舊的承載量 int newCap, newThr = 0;//聲明兩個變量存儲新的容量和裝載量 if (oldCap > 0) { if (oldCap >= MAXIMUM_CAPACITY) {//若是舊的容量大於容量最大值,不進行擴容,將Integer最大值給裝載容量 threshold = Integer.MAX_VALUE; return oldTab; } else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY)//將原有容量擴容一倍,但不能超過最大容量 newThr = oldThr << 1; // double threshold } else if (oldThr > 0) // initial capacity was placed in threshold newCap = oldThr;//若是容量爲零(數組爲空)但承載量不爲0,表示map中之前有過元素,將承載量賦值給容量 else { // zero initial threshold signifies using defaults newCap = DEFAULT_INITIAL_CAPACITY;// 第一次調用方法的時候初始化容量,16 newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);//初始化承載量,容量*0.75(承載因子) } if (newThr == 0) {//若是承載量爲零,則從新賦值 float ft = (float)newCap * loadFactor; newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE); } threshold = newThr;//覆蓋原有承載量 @SuppressWarnings({"rawtypes","unchecked"}) HashMap.Node<K,V>[] newTab = (HashMap.Node<K,V>[])new HashMap.Node[newCap]; table = newTab;//將原有節點數組指向根據新的容量建立新的節點數組 if (oldTab != null) {//若是原有數組不爲空,則將數組上的每個鏈表都拆分爲兩份,一份存儲在原有數組位置,一部分存儲在新擴容的數組位置,讓節點元素保持均勻分佈 for (int j = 0; j < oldCap; ++j) { HashMap.Node<K,V> e; if ((e = oldTab[j]) != null) { oldTab[j] = null; if (e.next == null) newTab[e.hash & (newCap - 1)] = e; else if (e instanceof HashMap.TreeNode) ((HashMap.TreeNode<K,V>)e).split(this, newTab, j, oldCap); else { // preserve order HashMap.Node<K,V> loHead = null, loTail = null; HashMap.Node<K,V> hiHead = null, hiTail = null; HashMap.Node<K,V> next; do { next = e.next; if ((e.hash & oldCap) == 0) { if (loTail == null) loHead = e; else loTail.next = e; loTail = e; } else { if (hiTail == null) hiHead = e; else hiTail.next = e; hiTail = e; } } while ((e = next) != null); if (loTail != null) { loTail.next = null; newTab[j] = loHead; } if (hiTail != null) { hiTail.next = null; newTab[j + oldCap] = hiHead; } } } } } return newTab; }
其中,爲何擴容耗時就很明顯了,擴容的流程是將原有數組擴大一倍,將數組上的每個鏈表一分爲二,均勻地分佈在數組上,而java8有引進了紅黑樹,當鏈表長度大於8時,將鏈表轉換爲紅黑樹,這樣大大加快了get(key)的速度。