map 主要有四個實現類:
HashMap、Hashtable、LinkedHashMap、TreeMap
LinkedHashMap:
有序,按照順序插入數據,根據Iterator遍歷時,先插的先得到。
TreeMap:
是SortedMap接口的實現類,默認按照鍵值的升序保存數據,也可以指定排序的比較器,key必須實現Comparable接口或者構造map時傳入自定義的Comparable,否則會拋ClassCastException異常
HashMap:
線程不安全,可用synchronizedMap或者ConcurrentHashMap代替便線程安全了
根據鍵的HashCode值來存儲數據
最多允許一條記錄的鍵爲null,允許多個值爲null
HashMap 詳解:
底層結構:數組 + 鏈表 + 紅黑樹
以上是hashmap的結構,每一個黑點表示一個Node,其中的Node是什麼呢,來看一下源碼:
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;
}
......此處省略部分方法,詳細請看jdk1.8源碼
}
Node是HashMap 的內部類,實現了Map.Entry接口,
hash用來定位數組索引的位置, next表示鏈表的下一個Node。 而Node[] table 表示哈系桶數組,初始化長度默認16
HashMap是使用哈希表來存儲的,哈希表爲了解決哈希衝突,用開放地址法和鏈地址法,HashMap採用鏈地址法,即數組和鏈表結合,每個數組元素上都有個鏈表結構,當數組被hash後,得到數組下標,然後把數組放在對應下標的鏈表裏。
下面先介紹下HashMap的構造函數,瞭解一下內部構造:
HashMap有四個構造函數,默認無參構造和參數是Map的構造函數是Java規範推薦實現的,還有兩個是HashMap專門提供的。
public HashMap(int initialCapacity, float loadFactor) {
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);
this.loadFactor = loadFactor;
this.threshold = tableSizeFor(initialCapacity);
}
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
public HashMap(Map<? extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
}
threshold: 所能容納的key-value對的極限,即HashMap擴容的閾值。等於容量(數組長度)乘以負載因子(length * loadFactor)
loadFactor:負載因子,默認0.75 (用於衡量散列表的空間使用程度)
modCount:字段主要用來記錄HashMap內部結構發生變化的次數
size:這個字段其實很好理解,就是HashMap中實際存在的鍵值對數量
length:數組(table)長度
負載因子默認值0.75是對空間和時間效率的一個平衡選擇。
//默認初始容量爲16(2的4次方),該數必須爲2的冪次
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
//最大容量 30
static final int MAXIMUM_CAPACITY = 1 << 30;
//默認負載因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//當put一個元素時,其鏈表長度達到8時將鏈表轉換爲紅黑樹
static final int TREEIFY_THRESHOLD = 8;
//鏈表長度小於6時,解散紅黑樹
static final int UNTREEIFY_THRESHOLD = 6;
//默認的最小的擴容量64,爲避免重新擴容衝突,至少爲4 * TREEIFY_THRESHOLD=32,即默認初始容量的2倍
static final int MIN_TREEIFY_CAPACITY = 64;
在HashMap中,哈系桶數組table的長度length設置成2的n次方,這是非常規設置(一般設置爲素數),這樣做主要是爲了取模和擴容時的優化,減少衝突。
儘管這樣,依然避免不了會出現拉鍊過長的情況,一旦過長,就會嚴重影響性能,jdk1.8中 進行了優化,加入了紅黑樹的結果,當鏈表長度過長(默認爲8)時,鏈表就轉爲紅黑樹結構。
下面講一下HashMap 的get,put方法的實現原理:
一 如何確定哈希桶數組索引的位置:
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
Hash算法包括:取key的hashCode值,高位運算,取模運算
二 HashMap的put方法:
下面看一下put方法的源碼以及大概的分析:
public V put(K key, V value) {
//對key的hashCode做hash
return putVal(hash(key), key, value, false, true);
}
//這裏onlyIfAbsent表示只有在該key對應原來的value爲null的時候才插入,也就是說如果value之前存在了,就不會被新put的元素覆蓋。
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
//若tab爲空,則創建
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//計算index(即根據key計算hash值得到數組的索引),並對null做處理。
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
//若節點key的hash值相同,則覆蓋value
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
//判斷這個鏈是否爲紅黑樹(TreeNode)
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
//鏈的長度大於8,則轉爲紅黑樹處理
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
//key 已經存在,直接覆蓋value
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
//超過最大容量,擴容
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
下圖爲對HashMap的put方法的分析過程圖示例:
簡單來講,HashMap的put方法是基於hashing原理,在put方法傳遞鍵值對時,先對鍵調用hashCode方法,其返回的hashCode的值來確定桶的位置,即數組的索引來存儲鍵,來作爲Map.Entry
三 HashMap的hash衝突問題:
產生原因: 在put方法在hashCode獲取hash值時,當put的元素越來越多時,難免產生不同的key產生相同的hash值問題,此時,便造成了hash衝突問題。
解決方法: 鏈表結構解決衝突問題
當存儲時,若hash值相同,則會找到相同的bucket的位置,此時發生碰撞,但由於是鏈表結構,每個Map.Entry都有一個next指針,
獲取時,調用equals方法。並且java8中的紅黑樹結構,更加大大減少了查詢時的複雜度。
減少碰撞方法:使用final修飾 或 不可變對象作爲鍵(例如:Integer,String),因爲這些已經重寫了equals方法和hashcode方法
另外:存入相同的key時,獲取的是後put的數據。
四 HashMap的擴容機制:
擴容就是當前容量不夠存儲數據時,進行擴容,resize方法。下面看一下簡單的擴容過程示意圖:
前半部分講到,默認的容量爲16,且可修改,但必須爲2的冪次,擴容就是將原來的容量擴大2倍,jdk1.8做了些優化,
因爲是擴大2倍,所以,元素要麼在遠位置,要麼在原位置移動2次冪的位置。
JDK1.7中rehash的時候,舊鏈表遷移新鏈表的時候,如果在新表的數組索引位置相同,則鏈表元素會倒置。
而1.8做了些優化,並不會重新計算hash值,當擴容後,n變爲2倍,n-1的範圍多出了一個bit字節,通過這個bit是0還是1判斷,0則是索引沒變,1則是索引變化,變爲“原索引+oldCap” resize()這塊的源碼暫時還未仔細看,有興趣的可以看看,一起溝通研究下。
五. 線程安全問題:
文章開頭說到,HashMap是線程不安全的,在併發環境中,會造成死循環,爲什麼呢?
以jdk1.7 爲例,重新調整map大小會出現競爭問題,在多線程中,map調整大小的過程中,即擴容重哈希時,存儲在鏈表的元素的次序會倒過來,因爲移動到新的bucket位置時,HashMap將元素放在頭部而不是尾部(避免尾部遍歷),這樣導致Entry鍊形成環,若競爭發生,這樣會發生死循環。導致線程不安全。多線程建議使用ConcurrentHashMap(採用了分段加鎖技術)