本文將為大家詳細講解Java中的Map集合,這是我們進行開發時經常用到的知識點,也是大家在學習Java中很重要的一個知識點,更是我們在面試時有可能會問到的問題。文章較長,乾貨滿滿,建議大家收藏慢慢學習。文末有本文重點總結,主頁有全系列文章分享。技術類問題,歡迎大家和我們一起交流討論! ...
本文將為大家詳細講解Java中的Map集合,這是我們進行開發時經常用到的知識點,也是大家在學習Java中很重要的一個知識點,更是我們在面試時有可能會問到的問題。
文章較長,乾貨滿滿,建議大家收藏慢慢學習。文末有本文重點總結,主頁有全系列文章分享。技術類問題,歡迎大家和我們一起交流討論!
前言
在上一篇文章中給大家講解了Java里的Set集合及其常用子類。現在我們已經掌握了Java里的兩大集合,最後還有另一大集合等待著我們學習,這就是Map集合。與之前的集合不太一樣,Map集合屬於雙列集合,該集合中的信息是key-value形式;而之前的LIst和Set都是單列集合,裡面的元素沒有key。
有些小伙伴可能會很好奇,我們已經學習了List和Set集合了,為什麼還要再搞出來一個Map集合呢?Map集合與Collection集合又有什麼不同呢? 要想搞清楚以上問題,我們可以考慮這麼一個需求。
假如我們利用List集合,來存儲一份Student學生信息,要求你根據name來查找某個指定的學生分數,你要怎麼實現?按照我們之前的經驗,常用的方式就是遍歷這個List,並挨個判斷name是否相等,找到後就返回指定元素。
其實這種需求非常的常見,通過一個關鍵詞來查詢對應的值。如果我們使用List來實現該需求,效率會非常的低,因為平均需要掃描完集合的一半元素才能確定。而如果使用Map這種key-value鍵值對映射表的數據結構,就可以通過key快速地查找到value值。
全文大約【8500】 字,不說廢話,只講可以讓你學到技術、明白原理的純乾貨!本文帶有豐富的案例及配圖視頻,讓你更好地理解和運用文中的技術概念,並可以給你帶來具有足夠啟迪的思考......
一. Map集合
1. 簡介
Map集合是一種以鍵值對形式存儲和操作數據的數據結構,建立了key-value之間的映射關係,常用於存儲和處理複雜的數據。同時Map也是一種雙列集合介面,它有多個實現類,包括HashMap、TreeMap、LinkedHashMap
等,最常用的是HashMap
類。其中,HashMap是按哈希演算法來實現存取鍵對象的,這是我們開發時最常用的Map子類,而TreeMap類則可以對鍵對象進行排序。
Map集合中的每個元素,都包含了一個鍵(key)和一個值(value),key和value組成了鍵-值的映射表,我們稱其為鍵值對。鍵用於唯一標識一個元素,值用於存儲該元素的數據,一般情況下,這個key和value可以是任何引用類型的數據。其中,鍵key是無序、無下標、不重覆的,最多只能有一個key為null。值value是無序、無下標、可重覆的,可以有多個value為null。
並且這個key和value之間是單向的一對一關係,即通過指定的key,總能找到唯一的、確定的value。當我們想從Map中取出數據時,只要給出指定的key,就能取出對應的value。
配套視頻如下 :戳我直達視頻
2. 特點
根據上面我們對Map概念的講解,把Map的主要特點給大家總結如下:
- Map 和 List 不同,Map是一種雙列集合;
- Map 存儲的是 key-value 的映射關係;
- Map 不保證順序 。在遍歷時,遍歷的順序不一定是 put() 時放入的 key 的順序,也不一定是 key 的排序順序。
3. 實現方式
在Java中,Map集合的實現方式主要有兩種:基於哈希表和基於樹結構。接下來給大家簡單介紹一下基於這兩種結構的Map集合。
3.1 基於哈希表的Map集合
基於哈希表的Map,其底層是基於哈希表作為數據結構的集合,主要的實現類是HashMap
。
在HashMap
中,每個元素都包含一個鍵和一個值。當我們在添加元素時,HashMap會根據鍵的哈希值計算出對應的桶(Bucket),並將元素存儲到該桶中。如果不同的鍵具有相同的哈希值,就會出現哈希衝突,此時HashMap會使用鏈表或紅黑樹等數據結構來解決衝突。基於哈希表的Map集合具有快速的添加、刪除和查找元素的特點,但元素的順序是不確定的。
3. 實現方式
在Java中,Map集合的實現方式主要有兩種:基於哈希表和基於樹結構。接下來壹哥給大家簡單介紹一下基於這兩種結構的Map集合。
3.1 基於哈希表的Map集合
基於哈希表的Map,其底層是基於哈希表作為數據結構的集合,主要的實現類是HashMap。在HashMap中,每個元素都包含一個鍵和一個值。當我們在添加元素時,HashMap會根據鍵的哈希值計算出對應的桶(Bucket),並將元素存儲到該桶中。如果不同的鍵具有相同的哈希值,就會出現哈希衝突,此時HashMap會使用鏈表或紅黑樹等數據結構來解決衝突。基於哈希表的Map集合具有快速的添加、刪除和查找元素的特點,但元素的順序是不確定的。
3.2 基於樹結構的Map集合
基於樹結構的Map集合,其底層是基於紅黑樹作為數據結構的集合,主要的實現類是TreeMap。在TreeMap中,每個元素也都包含一個鍵和一個值。我們在添加元素時,TreeMap會根據鍵的比較結果,將元素存儲到正確的位置上,使得元素可以按照鍵的升序或降序排列。基於樹結構的Map集合,具有快速查找和遍歷元素的特點,但添加和刪除元素的速度較慢。
4. 常用方法
Map集合的使用和其他集合類似,主要包括添加、刪除、獲取、遍歷元素等操作。
當我們調用put(K key, V value)方法時,會把key和value進行映射並放入Map。當調用V get(K key)時,可以通過key獲取到對應的value;如果key不存在,則返回null。如果我們只是想查詢某個key是否存在,可以調用containsKey(K key)方法。另外我們也可以通過 Map提供的keySet()方法,獲取所有key組成的集合,進而遍歷Map中所有的key-value對。
下表中就是Map里的一些常用方法,大家可以記一下,這些方法在Map的各實現子類中也都存在。
方法 | 描述 |
---|---|
clear() | 刪除Map中所有的鍵-值對 |
isEmpty() | 判斷Map是否為空 |
size() | 計算Map中鍵/值對的數量 |
put() | 將鍵/值對添加到Map中 |
putAll() | 將所有的鍵/值對添加到Map中 |
putIfAbsent() | 如果Map中不存在指定的鍵,則將指定的鍵/值對插入到Map中。 |
remove() | 刪除Map中指定鍵key的映射關係 |
containsKey() | 檢查Map中是否存在指定key對應的映射關係。 |
containsValue() | 檢查Map中是否存在指定的 value對應的映射關係。 |
replace() | 替換Map中指定key對應的value。 |
replaceAll() | 將Map中所有的映射關係替換成給定函數執行的結果。 |
get() | 獲取指定 key 對應對 value |
getOrDefault() | 獲取指定 key 對應對 value,如果找不到 key ,則返回設置的預設值 |
forEach() | 對Map中的每個映射執行指定的操作。 |
entrySet() | 返回Map中所有的鍵值對 |
keySet() | 返回Map中所有的key鍵 |
values() | 返回Map中所有的value值 |
merge() | 添加鍵值對到Map中 |
compute() | 對Map中指定key的值進行重新計算 |
computeIfAbsent() | 對Map中指定key的值進行重新計算,如果該key不存在,則添加到Map中 |
computeIfPresent() | 當key存在該Map時,對Map中指定key的值進行重新計算 |
與本節內容配套的視頻鏈接如下: 戳我直達視頻課程
5. 常用實現類
Java中有多個Map介面的實現類,接下來就給大家逐一簡單介紹一下。
5.1 HashMap
HashMap是Java中最常用的Map集合實現類,它基於哈希表實現,具有快速查找鍵值對的優點。 HashMap的存儲方式是無序的,也就是說,遍歷HashMap集合時,得到的鍵值對順序是不確定的。下麵是創建HashMap集合的代碼示例:
Map<String, Integer> hashMap = new HashMap<>();
5.2 TreeMap
TreeMap是Java中另一個常用的Map集合實現類,它基於紅黑樹實現,具有自動排序鍵值對的優點。TreeMap的存儲方式是有序的,也就是說,遍歷TreeMap集合時得到的鍵值對,是按照鍵的自然順序或指定比較器的順序排序的。下麵是創建TreeMap集合的代碼示例:
Map<String, Integer> treeMap = new TreeMap<>();
5.3 LinkedHashMap
LinkedHashMap是Java中另一個Map集合實現類,它繼承自HashMap,並保持了插入順序。也就是說,遍歷LinkedHashMap集合時,得到的鍵值對的順序是按照插入順序排序的。下麵是創建LinkedHashMap集合的代碼示例:
Map<String, Integer> linkedHashMap = new LinkedHashMap<>();
5.4 Hashtable
Hashtable是Java中另一個Map集合實現類,它與HashMap非常相似,但Hashtable是線程安全的。Hashtable的存儲方式是無序的,也就是說,遍歷Hashtable集合時,得到的鍵值對的順序是不確定的。下麵是創建Hashtable集合的代碼示例:
Map<String, Integer> hashtable = new Hashtable<>();
需要註意的是,由於Hashtable是線程安全的,所以在多線程環境中使用Hashtable的性能可能會較低,現在一般是建議使用ConcurrentHashMap。
5.5 ConcurrentHashMap
ConcurrentHashMap是Java中的另一個Map集合實現類,它與Hashtable非常相似,但是ConcurrentHashMap是線程安全的,並且性能更高。ConcurrentHashMap的存儲方式是無序的,也就是說,遍歷ConcurrentHashMap集合時,得到的鍵值對的順序是不確定的。下麵是創建ConcurrentHashMap集合的代碼示例:
Map<String, Integer> concurrentHashMap = new ConcurrentHashMap<>();
需要註意的是,雖然ConcurrentHashMap是線程安全的,但仍然需要註意多線程環境下的併發問題。
二. HashMap集合
1. 簡介
HashMap繼承自AbstractMap類,實現了Map、Cloneable、java.io.Serializable介面,如下圖所示:
HashMap是基於散列表實現的雙列集合,它存儲的是key-value鍵值對映射,每個key-value鍵值對也被稱為一條Entry條目。其中的 key與 value,可以是任意的數據類型,其類型可以相同也可以不同。但一般情況下,key都是String類型,有時候也可以使用Integer類型;value可以是任何類型。並且在HashMap中,最多只能有一個記錄的key為null,但可以有多個value的值為null。
HashMap中這些鍵值對(Entry)會分散存儲在一個數組當中,這個數組就是HashMap的主體。 預設情況下,HashMap這個數組中的每個元素的初始值都是null。但HashMap中最多只允許一條記錄的key為null,允許多條記錄的value為null。
另外HashMap是非線程安全的,即任一時刻如果有多個線程同時對HashMap進行寫操作,有可能會導致數據的不一致。 如果需要滿足線程的安全性要求,可以用 Collections.synchronizedMap() 方法使得HashMap具有線程安全的能力,或者使用ConcurrentHashMap來替代。
HashMap實現了Map介面,會根據key的hashCode值存儲數據,具有較快的訪問速度。另外HashMap是無序的,不會記錄插入元素的順序。我們一般是根據key的hashCode值來存取數據, 大多數情況下都可以直接根據key來定位到它所對應的值,因而HashMap有較快的訪問速度,但遍歷順序卻不確定。
2. 特點
根據上面的描述,把HashMap的核心特點總結如下:
- 基於哈希表實現,具有快速查找鍵值對的優點;
- HashMap的存儲方式是無序的;
- HashMap的key-value可以是任意類型,但key一般都是String類型,value類型任意;
- HashMap最多只能有一個記錄的key為null,但可以有多個value為null。
3. 常用操作
HashMap的使用方法和其他Map集合類似,主要包括添加元素、刪除元素、獲取元素、遍歷元素等操作。接下來會詳細地給大家介紹HashMap集合的常用操作。
3.1 創建Map集合對象
創建HashMap對象的方式有多種,我們可以使用構造函數,代碼如下:
// 使用構造函數創建HashMap對象
Map<String, Integer> map1 = new HashMap<>();
3.2 添加元素
向HashMap集合添加元素的方式和List集合類似,也是使用put()方法,下麵是向HashMap集合中添加元素的代碼示例:
public class Demo14 {
public static void main(String[] args) {
//HashMap
Map<String, String> map = new HashMap<>();
map.put("name","一一哥");
//map.put("name","壹哥");
map.put("age", "30");
map.put("sex", "男");
System.out.println("map="+map);
}
}
看到上面的代碼,有些小伙伴可能會問,如果我們往同一個Map中存儲兩個相同的key,但分別放入不同的value,這會有什麼問題嗎?
其實我們在Map中,重覆地放入 key-value 並不會有任何問題,但一個 key 只能關聯一個 value 。 因為當我們調用put(K key, V value)方法時,如果放入的key已經存在,put()方法會返回被刪除的舊value,否則就會返回null。所以Map中不會有重覆的key,因為放入相同的key時,就會把原有key對應的value給替換掉。
3.3 刪除元素
從HashMap集合中刪除元素的方式也和List集合類似,使用remove()方法。下麵是從HashMap集合中刪除元素的代碼示例:
//從HashMap集合中移除元素
map.remove("age");
3.4 獲取元素
從HashMap集合中獲取元素的方式和List集合不同,需要使用鍵來獲取值。HashMap
集合提供了兩種方法來獲取值,一種是使用get()方法
,另一種是使用getOrDefault()方法
。如果指定的鍵不存在,使用get()方法
會返回null
,而getOrDefault()方法
則會返回指定的預設值。下麵是從HashMap集合中獲取元素的代碼示例:
public class Demo15 {
public static void main(String[] args) {
//HashMap
Map<String, String> map = new HashMap<>();
map.put("name","一一哥");
map.put("age", "30");
map.put("sex", "男");
//根據key獲取指定的元素
String name = map.get("name");
String age = map.get("age");
//根據key獲取指定的元素,並設置預設值
String sex = map.getOrDefault("sex","男");
String height = map.getOrDefault("height","0");
System.out.println("[name]="+name+",[age]="+age+",[sex]="+sex+",[height]="+height);
}
}
3.5 遍歷元素
遍歷HashMap集合的方式和List集合不同,需要使用迭代器或者foreach迴圈遍歷鍵值對,下麵是遍歷HashMap集合的代碼示例:
/**
* @author 一一哥Sun
*/
public class Demo16 {
public static void main(String[] args) {
//HashMap
Map<String, String> map = new HashMap<>();
map.put("name","一一哥");
map.put("age", "30");
map.put("sex", "男");
//遍歷方式一:使用迭代器遍歷HashMap
//獲取集合中的entry條目集合
Set<Entry<String, String>> entrySet = map.entrySet();
//獲取集合中攜帶的Iterator迭代器對象
Iterator<Map.Entry<String, String>> iterator = entrySet.iterator();
//通過迴圈進行迭代遍歷
while (iterator.hasNext()) {
//獲取每一個Entry條目對象
Map.Entry<String, String> entry = iterator.next();
//獲取條目中的key
String key = entry.getKey();
//獲取條目中的value
String value = entry.getValue();
System.out.println(key + " = " + value);
}
//遍歷方式二:用foreach迴圈遍歷HashMap
for (Map.Entry<String, String> entry : map.entrySet()) {
String key = entry.getKey();
String value = entry.getValue();
System.out.println(key + " = " + value);
}
}
}
大家要註意,當我們使用Map時,任何依賴順序的邏輯都是不可靠的。比如,我們存入"A","B","C" 3個key,遍歷時,每個key會保證被遍歷一次且僅遍歷一次,但遍歷的順序完全沒有保證,甚至對於不同的JDK版本,相同的代碼遍歷輸出的順序都可能是不同的!所以我們在 遍歷Map時,要註意輸出的key是無序的!
3.6 判斷Map集合是否為空
判斷HashMap集合是否為空可以使用isEmpty()方法,如果Map集合為空,則返回true,否則返回false。下麵是判斷HashMap集合是否為空的代碼示例:
// 判斷HashMap是否為空
boolean isEmpty = map.isEmpty();
3.7 獲取Map集合的大小
獲取HashMap集合的大小可以使用size()方法,返回HashMap集合中鍵值對的數量,下麵是獲取HashMap集合大小的代碼示例:
// 獲取HashMap的大小
int size = map.size();
3.8 判斷Map集合是否包含指定的鍵或值
如果我們想判斷HashMap集合是否包含指定的鍵或值,可以使用containsKey()和containsValue()方法。如果Map集合包含指定的鍵或值,則返回true,否則返回false。下麵是判斷HashMap集合是否包含指定鍵或值的代碼示例:
// 判斷HashMap是否包含指定鍵
boolean containsKey = map.containsKey("name");
// 判斷HashMap是否包含指定值
boolean containsValue = map.containsValue("一一哥");
3.9 替換Map集合中的鍵值對
如果我們想替換HashMap集合中的鍵值對,可以使用replace()方法將指定鍵的值替換成新的值。如果指定的鍵不存在,則不進行任何操作。下麵是替換HashMap集合中的鍵值對的代碼示例:
// 替換HashMap中的鍵值對
map.replace("name", "壹哥");
3.10 合併兩個Map集合
如果我們想合併兩個HashMap集合,可以使用putAll()方法,將另一個HashMap集合中所有的鍵值對,添加到當前的HashMap集合中。下麵是合併兩個HashMap集合的代碼示例:
public class Demo16 {
public static void main(String[] args) {
//HashMap
Map<String, String> map1 = new HashMap<>();
map1.put("name","一一哥");
map1.put("age", "30");
map1.put("sex", "男");
// 創建另一個TreeMap集合
Map<String, String> map2 = new HashMap<>();
map2.put("height", "180");
map2.put("salary", "5w");
//將map1中的鍵值對添加到map2中
map2.putAll(map1);
System.out.println("map2="+map2);
}
}
3.11 獲取Map集合中所有的鍵或值
如果我們想獲取HashMap集合中所有的鍵或值,可以分別使用keySet()和values()方法。這兩個方法會返回一個Set集合和一個Collection集合,它們包含了HashMap集合中所有的鍵或值。下麵是獲取HashMap集合中所有鍵或值的代碼示例:
public class Demo18 {
public static void main(String[] args) {
//HashMap
Map<String, String> map = new HashMap<>();
map.put("name","一一哥");
map.put("age", "30");
map.put("sex", "男");
// 獲取HashMap中所有的鍵
Set<String> keySet = map.keySet();
for(String key : keySet) {
System.out.println("key="+key);
}
// 獲取HashMap中所有的值
Collection<String> values = map.values();
for(String value:values) {
System.out.println("value"+value);
}
}
}
4. 原理概述(重點)
作為開發時最常用的Map集合,HashMap在我們面試時被問到的概率非常高,尤其是關於其原理、數據結構、衝突解決、併發、擴容等相關的內容,更是經常被問到。
如果我們想要瞭解HashMap的底層原理,首先得知道HashMap的底層數據結構,而這個數據結構在不同的JDK版本中是不同的。我們可以把HashMap的底層數據結構分為兩大版本,即JDK 7及其以前的版本 和 JDK 8及其以後的版本。 大家註意,本文主要是結合JDK 8的源碼,給大家講解HashMap的底層原理。
- 在JDK 7及其以前版本的HashMap中,其底層的數據結構是 數組+鏈表 ;
- 而從JDK 8開始則採用 數組+鏈表+紅黑樹 的數據結構,其中的 數組是Entry類型或者說是Node類型數組 。
5. hash衝突解決機制
因為HashMap底層會使用Hash演算法來處理數據的存取,當數據非常多時就有一定的概率出現hash衝突,其解決過程如下。
- 當我們往HashMap中存儲數據時,首先會利用hash(key)方法 計算出key的hash值,再利用該 hash值 與 HashMap數組的長度-1 進行 與運算,從而得到該key在數組中對應的下標位置;
- 如果該位置上目前還沒有存儲數據,則直接將該key-value鍵值對數據存入到數組中的這個位置;
- 如果該位置目前已經有了數據,則把新的數據存入到一個鏈表中;
- 當鏈表的長度超過閾值(JDK 8中該值為8)時,會將鏈表轉換為紅黑樹(轉換為紅黑樹還需要滿足其他的條件,鏈表長度達到閾值只是其中的一個條件)。
通過這樣的機制,HashMap就解決了存值時可能產生的哈希衝突問題,並可以大大提高我們的查找效率。
當然由於HashMap極其重要,它的內容非常多,尤其是原理性的內容更多。但由於篇幅限制,不會在本文中過多地講解HashMap原理等的內容
三. Hashtable集合
1. 簡介
Hashtable也是Java中的一個Map集合,它與HashMap非常相似,但Hashtable是線程安全的,而HashMap不是線程安全的。Hashtable也可以存儲鍵值對,並可以通過鍵快速查找到對應的值,Hashtable的鍵和值也都可以是任何類型的對象。
因為Hashtable是線程安全的,因此適用於多線程環境。在多線程環境中,如果需要對Hashtable進行多個操作,需要使用synchronized關鍵字來保證線程安全。但需要我們註意的是,在多線程環境中使用Hashtable可能會影響性能,所以如果不需要保證線程安全,請儘量使用HashMap。
Hashtable集合的底層結構主要是數組+鏈表,數組是Entry數組,鏈表也是用Entry來實現的 。 所以Hashtable的底層核心,其實也是基於哈希表,它使用哈希函數將鍵映射到哈希表中的一個位置,從而實現快速查找。另外Hashtable的存儲方式是無序的,也就是說,遍歷Hashtable集合得到的鍵值對的順序是不確定的。
2. 常用方法
Hashtable與HashMap類似,它的常用方法也與HashMap一樣:
- put(K key, V value):將指定的鍵值對存儲到Hashtable中。
- get(Object key):返回指定鍵所對應的值,如果不存在該鍵則返回null。
- remove(Object key):從Hashtable中移除指定鍵所對應的鍵值對。
- containsKey(Object key):判斷Hashtable中是否包含指定的鍵。
- containsValue(Object value):判斷Hashtable中是否包含指定的值。
- size():返回Hashtable中鍵值對的數量。
- clear():移除Hashtable中的所有鍵值對。
3. 基本使用
下麵是一個簡單的Hashtable集合示例,演示瞭如何創建Hashtable集合、存儲鍵值對、獲取值、遍歷集合等操作。
import java.util.Hashtable;
import java.util.Map;
public class Demo19 {
public static void main(String[] args) {
// 創建Hashtable集合
Map<String, Integer> hashtable = new Hashtable<>();
// 存儲鍵值對
hashtable.put("apple", 10);
hashtable.put("banana", 20);
hashtable.put("orange", 30);
// 獲取值
int value1 = hashtable.get("apple");
int value2 = hashtable.get("banana");
int value3 = hashtable.get("orange");
System.out.println("apple: " + value1);
System.out.println("banana: " + value2);
System.out.println("orange: " + value3);
// 移除鍵值對
hashtable.remove("orange");
// 遍歷集合
for (Map.Entry<String, Integer> entry : hashtable.entrySet()) {
String key = entry.getKey();
int value = entry.getValue();
System.out.println(key + ": " + value);
}
// 清空集合
hashtable.clear();
}
}
其他方法的使用與HashMap基本一致,就不再一一細說了。
四. ConcurrentHashMap集合
1. 簡介
由於在多線程環境下,常規的HashMap可能會在數組擴容及重哈希時出現 死迴圈、臟讀 等線程安全問題,雖然有HashTable、Collections.synchronizedMap()可以取代HashMap進行併發操作,但因它們都是利用一個 全局的synchronized鎖 來同步不同線程之間的併發訪問,因此性能較差。
所以Java就從JDK 1.5版本開始,在J.U.C(java.util.concurrent併發包) 中引入了一個高性能的併發操作集合---ConcurrentHashMap(可以簡稱為CHM)。該集合是一種線程安全的哈希表實現,相比Hashtable和SynchronizedMap,在多線程場景下具有更好的性能和可伸縮性。
並且ConcurrentHashMap集合在讀數據時不需要加鎖,寫數據時會加鎖,但鎖的粒度較小,不會對整個集合加鎖。而其內部又大量的利用了 volatile,final,CAS等lock-free(無鎖併發)技術,減少了鎖競爭對於性能的影響,具有更好的寫併發能力,但降低了對讀一致性的要求。 因此既保證了併發操作的安全性,又確保了讀、寫操作的高性能,可以說它的併發設計與實現都非常的精巧。
另外ConurrentHashMap中的key與value都不能為null,否則會產生空指針異常!
2. ConcurrentHashMap類關係
ConcurrentHashMap與HashMap具有相同的父類AbstractMap,他倆可以說是“親兄弟”,所以ConcurrentHashMap在一般的特性和使用上與HashMap基本是一致的,甚至很多底層原理也是相似的。
但兩者所在的包是不同的,ConcurrentHashMap是在java.util.concurrent包中,HashMap是在java.util包中,我們可以參考下圖:
以上所述的ConcurrentHashMap概念及特征,是不區分版本的,但實際上不同版本的ConcurrentHashMap內部實現差異很大,所以面試時經常會被問到不同版本之間的差異、各自特征。接下來會針對JDK 7 與 JDK 8 這兩個經典版本分別進行闡述。
3. 實現原理
3.1 併發控制機制
在JDK 7中,ConcurrentHashMap的核心機制是分段鎖(Segment),每個Segment內部維護了一個哈希表,且這個哈希表是線程安全的。而ConcurrentHashMap中的每個操作,都是先對所在的Segment進行加鎖,然後再執行具體的操作。
當多個線程對不同的Segment進行操作時,它們之間是併發的。當多個線程對同一個Segment進行操作時,它們會競爭鎖,但不會影響到其他Segment的操作。這種機制有效地降低了鎖的粒度,提高了併發訪問效率。
ConcurrentHashMap的另一個優點是支持可伸縮性。當需要增加ConcurrentHashMap的容量時,我們只需要增加Segment的數量即可,這種機制使得ConcurrentHashMap在高併發場景下具有良好的可伸縮性。
3.2 JDK 7版本的ConcurrentHashMap
在JDK 7版本中,ConcurrentHashMap和HashMap的設計思路其實是差不多的,但為了支持併發操作,做了一定的改進。比如ConcurrentHashMap中的數據是一段一段存放的,我們把這些分段稱之為Segment分段,在每個Segment分段中又有 數組+鏈表 的數據結構。
預設情況下ConcurrentHashMap把主幹分成了 16個Segment分段,並對每一段都單獨加鎖,我們把這種設計策略稱之為 "分段鎖",ConcurrentHashMap就是利用這種分段鎖機制進行併發控制的。JDK 7中ConcurrentHashMap的基本數據結構如下圖所示:
在理想狀態下,ConcurrentHashMap可以 同時支持16個線程 執行併發寫操作,以及任意數量的線程進行併發讀操作。在寫操作時,通過分段鎖技術,只對所操作的段加鎖而不會影響其它分段,且在讀操作時(幾乎)不需要加鎖。
3.3 JDK 8版本的ConcurrentHashMap
在JDK 8中,ConcurrentHashMap
相對於JDK 7版本做了很大的改動,從實現的代碼量上就可以看出來,JDK 7中ConcurrentHashMap不到2000行代碼,而JDK 8中則有6000多行代碼。
JDK 8中放棄了臃腫的Segment設計,取而代之採用了 Node數組 + 鏈表 + 紅黑樹 + CAS + Synchronized 技術來保證併發安全,實現併發控制操作。但是在ConcurrentHashMap的實現中保留了 Segment 的定義,這是為了 保證序列化時的相容性,但並沒有任何結構上的用處。在JDK 8中用synchronized替換ReentrantLock的原因大致如下:
- 減少記憶體開銷: 如果使用ReentrantLock,就需要節點繼承AQS來獲得同步支持,這增加了記憶體開銷,而JDK 8中只有頭節點需要進行同步。
- 內部優化: synchronized是JVM直接支持的,JVM能夠在運行時作出相應的優化措施,比如 鎖粗化、鎖消除、鎖自旋等。
4. 基本使用
ConcurrentHashMap支持與HashMap相同的常用操作,如put、get、remove等。下麵介紹一些常用操作。
4.1 插入元素
ConcurrentHashMap的put方法用於向集合中插入一個鍵值對,如果鍵已經存在,則會更新對應的值。下麵是向ConcurrentHashMap中插入元素的示例:
import java.util.concurrent.ConcurrentHashMap;
public class Demo19 {
public static void main(String\[] args) {
// 創建ConcurrentHashMap集合
ConcurrentHashMap\<String, Integer> map = new ConcurrentHashMap<>();
// 插入元素
map.put("apple", 10);
map.put("banana", 20);
map.put("orange", 30);
// 輸出元素
System.out.println(map);
}
}
根據上面代碼的執行結果可知,ConcurrentHashMap中的鍵值對是無序的。
4.2 獲取元素
ConcurrentHashMap
的get方法用於獲取指定鍵對應的值,如果鍵不存在,則返回null。下麵是一個從ConcurrentHashMap中獲取元素的示例:
import java.util.concurrent.ConcurrentHashMap;
public class Demo20 {
public static void main(String[] args) {
// 創建ConcurrentHashMap集合
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
// 插入元素
map.put("apple", 10);
map.put("banana", 20);
map.put("orange", 30);
// 獲取元素
Integer value = map.get("apple");
System.out.println(value);
}
}
4.3 刪除元素
ConcurrentHashMap的remove方法用於從集合中刪除指定鍵的元素,下麵是一個從ConcurrentHashMap中刪除元素的示例:
import java.util.concurrent.ConcurrentHashMap;
public class Demo21 {
public static void main(String[] args) {
// 創建ConcurrentHashMap集合
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
// 插入元素
map.put("apple", 10);
map.put("banana", 20);
map.put("orange", 30);
// 刪除元素
map.remove("apple");
// 輸出元素
System.out.println(map);
}
}
五. 結語
至此,我們就把Map及其子類學習完了,現在你知道Map有什麼特性及其用法了嗎?接下來我們簡單總結一下今天的重點吧:
- Map是一種映射表,可以通過key快速查找value;
- 可以通過 for each 遍歷 keySet() ,也可以通過 for each 遍歷 entrySet() ,直接獲取 key-value ;
- 最常用的Map是HashMap;
- Hashtable是線程安全的集合,底層結構主要是數組+鏈表;
- ConcurrentHashMap可以良好地進行併發處理,是一種線程安全且高效的集合。