HashMap(JDK 8):
爲什麼用HashMap:
- HashMap 是一個散列桶(數組和鏈表/紅黑樹),它存儲的內容是鍵值對 key-value 映射
- 當hash衝突越少時(鏈表長度短,紅黑樹深度小),查找的時間複雜度越接近O(1);添加,刪除,修改的時間複雜度都爲O(1)
- HashMap 是非 synchronized,所以對 HashMap的操作很快
- HashMap、LinkedHashMap 可以接受 null 鍵和值,TreeMap可以接收值爲null,其餘Map的K 、V 不能接受null
HashMap的工作原理:
- HashMap是基於hashing原理的,使用 put(key, value) 存儲對象到 HashMap 中,使用 get(key) 從 HashMap 中獲取對象。
- 當使用 put() 方法傳遞鍵和值時,先對鍵調用 hashCode() 方法,然後找到bucket中的位置來存儲Node對象。當對鍵計算得到的hash值出現衝突時,採用鏈表尾插法鏈接多個Entry,當鏈表元素超過8轉紅黑樹,當紅黑樹節點少於6轉爲鏈表。
- 當使用get()方法時,使用鍵對象的hashcode 找到 bucket 位置。如果該 bucket 位置,存在多個Entry,開始從頭遍歷調用 keys.equals() 方法去找到鏈表中正確的節點,最終找到要找的值對象。
爲什麼紅黑樹與鏈表的轉換會使用8 / 6:
-
爲避免紅黑樹、鏈表的重複轉換,使用當樹中節點少於6時,鏈表節點大於8時,才發生轉換。當元素少於8時,使用Node佔用空間少,操作速度快。
-
紅黑樹的插入、刪除和遍歷的最壞時間複雜度都是log(n),TreeNode佔用更多的空間,只有當擁有足夠的元素,纔有使用紅黑樹的必要。桶中的某一位置出現hash衝突是一個隨機事件,發生概率遵循泊松分佈,桶中某一位置Entry對長度超過8的概率非常小,在源碼中有舉例說明,故選擇8。
HashMap擴容機制:
使用空參構造方法:
Map容量爲16,當Map中元素個數大於12(= DEFAULT_INITIAL_CAPACITY(16) * DEFAULT_LOAD_FACTOR(0.75))後,擴容爲大於16的第一個2的冪(32)
使用指定初始容量的構造方法:
Map容量爲大於等於傳入參數的第一個2的冪(如:傳入7,則Map容量爲:8;傳入8,則Map容量爲:8),當Map中元素個數大於capacity (= 初始化的容量 * DEFAULT_LOAD_FACTOR(0.75))後,擴容爲大於capacity的第一個2的冪
《阿里Java開發手冊》建議設置initialCapacity=(需要存儲的元素個數 / 負載因子(默認:0.75))+1,來減少重建hash表對性能的消耗。
爲什麼負載因子選擇0.75:
如果使用 0.5,那麼 Map 中未利用的空間將會隨着容量的增長而增長,如果使用1,那麼在使用put方法時,將會延長等待時間。在源碼中也只是說了“默認負載因子(0.75)在時間和空間成本上提供了很好的折衷”,負載因子的選擇可以按實際項目情況來。
擴容時,需要重新計算hash嗎?
在 JDK 8 之前需要重新計算 hash 值,JDK 8 開始,hash 值不重新計算,只是根據不同 hash 值和之前的 bucket 容量進行與運算( (e.hash & oldCap) == 0 ),如果結果爲0,元素在 bucket 中的索引值不變;如果不爲0,元素的索引值變爲 j(原索引)+OldCap(原bucket大小) 。參考下列源碼:
final Node<K,V>[] resize() {
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) {
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;
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
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"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
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 TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // preserve order
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
-start-------------------------
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;
}
}
}
-end-------------------------
}
}
return newTab;
}
在上面標記的一段中,(e.hash & oldCap) == 0 是爲了求得當前 hash 值與 oldCap(=2^n)二進制的最高位1,是否相同,同則結果爲1,不同結果爲0。例如:16 & 32 = 0,hash值爲16的元素在擴容後索引值不變; 17 & 16 !=0,hash值爲17的元素在擴容後索引值變爲 j +16
HashMap 如何求得 key 的 hash 值?爲什麼容量使用的是2的n次方?HashMap 是如何通過 hash 值定位某一個元素的?
JDK 8 HashMap 中的源碼:
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
//獲取某一元素在桶中的位置
int index=(n - 1) & hash;//n = tab.length ,tab爲散列桶的數組
hash 值是 32bit 的 int 類型,不能使用 hash 值的範圍(40億個位置)爲散列桶數組的容量,故需要將 hash 值縮小到適合的大小。
通過右移(>>>)和異或(^)運算,將 hash 值的高16位與低16位進行異或運算,這樣做是爲了讓高低位的信息混合,再把得到的 hash 值和散列桶容量 - 1 進行與(&)運算,得到 hash 值的最後幾位,這便是散列桶數組中的索引。
在進行 與運算 時,爲了得到在數組範圍內的索引,就必須要和 2^n(散列桶容量) - 1 (該式保證低位全爲1)做運算。
LinkedHashMap(按鍵值對添加順序排序):
public class LinkedHashMap<K,V> extends HashMap<K,V> implements Map<K,V>{... ...}
EnumMap:
特點:
-
用Enum類型作爲key的HashMap
-
可以根據Enum類型的key,快速定位,而不需要對key求hashcode(),故效率高
public class EnumMapTest {
@Test
public void enumMapTest() {
System.out.println(Color.GREEN.ordinal() + " : " + Color.GREEN.name());
EnumMap<Color, String> map = new EnumMap<>(Color.class);
map.put(Color.RED, "red");
map.put(Color.BLACK, "black");
map.forEach((k, v) -> {
System.out.println(k + " : " + v);
});
//RED : red
//BLACK : black
}
}
enum Color {
RED, GREEN, YELLOW, BLACK
}
WeakHashMap:
定義:
其key是弱引用類型,在GC執行垃圾回收時,會移除弱引用對象(當將鍵值對中的key設置爲null時,其指向的對象便是弱引用對象),所以,WeakHashMap的size()方法返回值會隨着程序的運行變小,isEmpty()方法的返回值會從false變成true等。
WeakHashMap可作爲緩存使用,在Tomcat中有具體的使用。
TreeMap(按key排序):
TreeMap的底層是通過紅黑樹來實現的,其查找、修改、增加的時間複雜度都爲O( log n )。
TreeMap的排序是因爲實現了NavigableMap接口,而它又繼承了SortedMap接口。
定義TreeMap的key排序規則,默認是對key排升序:
/**
* @Author Snail
* @Describe 自定義treemap的升序與降序
* @CreateTime 2019/8/16
*/
public class TestTreeMap {
@Test
public void treeMapOrder(){
TreeMap<Integer,String> treeMap=new TreeMap(new Comparator() {
@Override
public int compare(Object o1, Object o2) {
//o1-o2:表示用升序,o2-o1:爲降序
return (Integer) o2-(Integer) o1;
}
});
treeMap.put(1,"aaa");
treeMap.put(3,"bbb");
treeMap.put(2,"bbb");
treeMap.put(9,"bbb");
treeMap.put(5,"bbb");
for(Map.Entry<Integer,String> entry:treeMap.entrySet()){
System.out.println(entry.getKey() + "::::::::::::::::" + entry.getValue());
}
}
@Test
public void treeMapOrderBean(){
TreeMap<Bean,String> treeMap=new TreeMap(new Comparator<Bean>() {
@Override
public int compare(Bean o1, Bean o2) {
//o1-o2:表示用升序,o2-o1:爲降序
return o2.getOrder()-o1.getOrder();
}
});
treeMap.put(new Bean(2),"aaa");
treeMap.put(new Bean(5),"aaa");
treeMap.put(new Bean(3),"aaa");
treeMap.put(new Bean(6),"aaa");
treeMap.put(new Bean(10),"aaa");
treeMap.put(new Bean(1),"aaa");
for(Map.Entry<Bean,String> entry:treeMap.entrySet()){
System.out.println(entry.getKey().getOrder() + "::::::::::::::::" + entry.getValue());
}
}
}
class Bean{
private Integer order;
public Bean(int i) {
order=i;
}
public Integer getOrder() {
return order;
}
public void setOrder(Integer order) {
this.order = order;
}
}
HashSet(基於HashMap實現的HashSet):
HashSet中,所有鍵值對存儲的值都是PRESENT(源碼:private static final Object PRESENT = new Object();),HashSet的add方法將值存入了key(key的存儲,滿足HashMap的put方法),這也就滿足了 Set 中元素不重複的特性。
ConcurrentHashMap(高效率的線程安全的Map容器)
JDK 1.7 下的分段式鎖
將 HashMap 中的散列桶數組分成多段 Segment 存儲,加鎖時使用 Segment 對象,當一個線程佔用鎖操作其中一段數據的時候,其他段的數據也能被其他線程操作;並配合 CAS 完成對數據的添加。
//Segment.put(...) 方法中嘗試獲取鎖的代碼
HashEntry<K,V> node = tryLock() ? null : scanAndLockForPut(key, hash, value);
JDK 1.8 在添加元素時,對數組頭節點加鎖
添加元素源碼的大致流程: 在擴容時,如果當前數組位置爲空,則直接添加;如果當前數組位置的節點 在擴容 ( (fh = f.hash) == MOVED),則當前線程加入擴容操作;如果當前數組位置不爲空,則使用 synchronized 加鎖數組桶中的頭節點,完成加入元素操作。所以,ConcurrentHashMap支持多線程擴容。
Map的遍歷建議:
- JDK 8 後建議使用 map.forEach()
map.forEach((k, v) -> {
System.out.println(k + " " + v);
});
2. 使用entrySet來遍歷map集合
for (Map.Entry entry:map.entrySet()){
System.out.println(entry.getKey()+" : "+entry.getValue());
}
Map間的比較:
-
HashMap是無序的,有序的是TreeMap(可按key排升降序)和LinkedHashMap(按插入順序排序)。
-
HashMap是線程不安全的。在JDK 8下,多線程的put操作會出現數據覆蓋的情況,鏈表的元素插入使用尾插法;在JDK 7下,由於在擴容期間,擴容操作會對新鏈表使用到頭插法,多個線程操作時就可能出現循環鏈表,在獲取該位置的元素時,就會導致應用卡死。但如果設置了初始容量後不會出現擴容操作,那麼就不會發生該情況。
-
HashMap 與 Hashtable 之間的區別有那些?
-
HashMap 是線程不安全的,適合在單線程的環境下使用;Hashtable 是線程安全的,可以(但不推薦)在多線程環境下使用
-
HashMap 允許 key-value 爲 Null ,Hashtable 不允許 key-value 爲 Null
-
HashMap 的 initialCapacity=16 ,擴容規則是大於當前容量的第一個2的n次方;Hash table的 initialCapacity=11,擴容規則爲 oldCapacity * 2 + 1
-
兩者對 key 求 hash 時運算規則不同,HashMap 的運算規則更加的高效與隨機。
-
線程安全的幾種Map:
Hashtable<>() 與 Coolections.synchronizedMap(new HashMap<>()) 都是線程安全的,對每個方法添加synchronized修飾,同一時刻,僅允許一個線程對容器進行操作,導致對容器的訪問變成串行化的了,故效率低下。
ConcurrentHashMap<>():JDK1.8使用 Synchronized + CAS 保證線程安全;CAS是一個樂觀鎖,通過判斷標記決定這個線程能否操作。效率高,推薦使用。