Map接口
HashMap和Hashtable的區(qū)別
- 線程是否安全:HashMap是非線程安全的,Hashtable是線程安全的,因?yàn)镠ashtable內(nèi)部的方法基本都經(jīng)過synchronized修飾(這是很老的一個(gè)實(shí)現(xiàn),如果現(xiàn)在需要保證線程安全的話推薦使用ConcurrentHashMap)
- 效率:因?yàn)榫€程安全的問題,HashMap要比Hashtable的效率高一些,另外Hashtable基本被淘汰,不建議在代碼中使用。
- 對(duì)Null key和Null value的支持:HashMap可以存儲(chǔ)null的key和value。但是null作為鍵只能有一個(gè)。null作為值可以有多個(gè)。Hashtable不允許有null鍵和null值,否則會(huì)拋出空指針異常。
-
初始容量大小和每次擴(kuò)充容量大小不同:
- 創(chuàng)建時(shí)如果不指定容量初始值,Hashtable默認(rèn)的初始大小為11,之后每次擴(kuò)容容量變?yōu)樵瓉淼?n+1.HashMap默認(rèn)初始化大小16.之后每次擴(kuò)容,容量都變?yōu)樵彽?倍。
- 創(chuàng)建時(shí)如果給定了容量初始值,Hashtable會(huì)直接使用你給定的大小。而HashMap會(huì)將其擴(kuò)容為2的冪次方發(fā)小。也就是說HashMap永遠(yuǎn)使用2的冪次方作為哈希表的大小。
- 底層數(shù)據(jù)結(jié)構(gòu):JDK1.8以后HashMap在解決哈希沖突時(shí)有了較大的變化,當(dāng)鏈表長(zhǎng)度大于閾值(默認(rèn)8)(將鏈表轉(zhuǎn)化成紅黑樹前會(huì)判斷,如果當(dāng)前數(shù)組長(zhǎng)度小于64,那么會(huì)先進(jìn)性數(shù)組擴(kuò)容,而不是轉(zhuǎn)化成紅黑樹)時(shí),會(huì)將鏈表轉(zhuǎn)化 成紅黑樹,以減少搜索時(shí)間,Hashtable沒有這樣的機(jī)制。
HashMap和HashSet的區(qū)別
HashSet的源碼中很清楚的寫的:HashSet的底層是基于HashMap實(shí)現(xiàn)的。HashSet的源碼非常少,因?yàn)槌薈lone(),writeObject(),readObject()等是HashSet自己不得不實(shí)現(xiàn)的以外,其他的方法都是直接調(diào)用HashMap中的方法.

HashMap和TreeMap的區(qū)別
TreeMap和HashMap都繼承自AbstractMap,但是需要注意的是TreeMap它還實(shí)現(xiàn)了NavigableMap接口和SortedMap接口。
實(shí)現(xiàn)了NavigableMap接口讓TreeMap有了對(duì)集合內(nèi)元素的搜索的能力。
實(shí)現(xiàn)SortedMap接口讓TreeMap有了對(duì)集合中的元素根據(jù)鍵排序的能力。默認(rèn)是按照key的升序排序,不過我們也可以指定排序的比較器。
/**
* @author shuang.kou
* @createTime 2020年06月15日 17:02:00
*/
public class Person {
private Integer age;
public Person(Integer age) {
this.age = age;
}
public Integer getAge() {
return age;
}
public static void main(String[] args) {
TreeMap<Person, String> treeMap = new TreeMap<>(new Comparator<Person>() {
@Override
public int compare(Person person1, Person person2) {
int num = person1.getAge() - person2.getAge();
return Integer.compare(num, 0);
}
});
treeMap.put(new Person(3), "person1");
treeMap.put(new Person(18), "person2");
treeMap.put(new Person(35), "person3");
treeMap.put(new Person(16), "person4");
treeMap.entrySet().stream().forEach(personStringEntry -> {
System.out.println(personStringEntry.getValue());
});
}
}
這個(gè)代碼的執(zhí)行可以看出TreeMap已經(jīng)按年齡升序來排列了。當(dāng)然也可以用lambda來實(shí)現(xiàn)排序:
TreeMap<Person, String> treeMap = new TreeMap<>((person1, person2) -> {
int num = person1.getAge() - person2.getAge();
return Integer.compare(num, 0);
});
綜上,相比于HashMap,TreeMap主要是多了對(duì)集合中的元素根據(jù)鍵值排序的能力以及對(duì)集合內(nèi)元素的搜索的能力。
HashSet如何檢查重復(fù)
據(jù)說java8之前的版本:把對(duì)象加入HashSet的時(shí)候,會(huì)先計(jì)算對(duì)象的hashcode值來判斷對(duì)象加入的位置,同事也會(huì)與其他加入的對(duì)象的hashCode值比較,如果沒有相同的,HashSet會(huì)假設(shè)對(duì)象沒有重復(fù)出現(xiàn),如果發(fā)現(xiàn)有hashCode值相同的對(duì)象,會(huì)調(diào)用equals()方法來檢查hashCode相等的對(duì)象是否真的相同。如果兩者相同說明有重復(fù),不會(huì)加入成功。
而在JDK1.8中,HashSet的add方法只是簡(jiǎn)單的調(diào)用了一下HashMap的put方法,并且判斷了一下返回值以確保是否有重復(fù)元素。下面是源碼截圖:

也就是說在1.8中,無論HashSet中是否已經(jīng)存在了某元素,HashSet都會(huì)直接插入。只是會(huì)在add方法的返回值處告訴我們插入前是否已經(jīng)存在相同的元素。
HashCode與equals的相關(guān)規(guī)定:
- 如果兩個(gè)對(duì)象相等,則hashCode一定也是相同的
- 如果兩個(gè)對(duì)象相等,equals判斷一定是true
- 兩個(gè)對(duì)象有相同的hashCode值,他們也不一定是相等的
- 綜上,equals方法被覆蓋過,則hashCode方法也必須被覆蓋。
- hashCode默認(rèn)行為是對(duì)堆上的對(duì)象產(chǎn)生獨(dú)特值,如果沒有重寫hashCode,則該class的兩個(gè)對(duì)象無論如何都不會(huì)相等。
HashMap的底層實(shí)現(xiàn)
JDK1.8之前HashMap底層是數(shù)組和鏈表結(jié)合在一起使用,也就是鏈表散列。HashMap通過key的hashCode經(jīng)過擾動(dòng)函數(shù)處理后得到hash值,然后通過(n-1)&hash判斷當(dāng)前元素存放的位置(這里n是數(shù)組的長(zhǎng)度)。如果當(dāng)前位置存在元素的話,就判斷該元素與要存入的元素的hash值以及key是否相同,如果相同的話直接覆蓋,不相同就通過拉鏈法解決沖突。
所謂擾動(dòng)函數(shù)指的就是HashMap的hash方法,使用哈希方法也就是擾動(dòng)函數(shù)是為了防止一些實(shí)現(xiàn)比較差的hashCode方法,從而減少哈希碰撞。
所謂“拉鏈法”就是:將鏈表和數(shù)組相結(jié)合,也就是說創(chuàng)建一個(gè)鏈表數(shù)組,數(shù)組中每一個(gè)格就是一個(gè)鏈表,若遇到哈希沖突,將沖突的值加到鏈表中即可。

相比于之前的版本,jdk1.8之后在解決哈希沖突的時(shí)候有了較大的變化:當(dāng)鏈表長(zhǎng)度大于閾值(默認(rèn)是8.但是將鏈表轉(zhuǎn)化紅黑樹前會(huì)判斷當(dāng)前數(shù)組長(zhǎng)度,數(shù)組小于64的話會(huì)先數(shù)組擴(kuò)容,而不是轉(zhuǎn)化紅黑樹)時(shí),將鏈表轉(zhuǎn)化為紅黑樹,以減少搜索時(shí)間。

TreeMap,TreeSet,以及1.8以后的HashMap底層都用到了紅黑樹,紅黑樹就是為了解決二叉查找樹的缺陷,因?yàn)槎娌檎覙淠承┣闆r下會(huì)退化成一個(gè)線性結(jié)構(gòu)。
下圖可以看做是一個(gè)簡(jiǎn)單的HashMap添加元素的步驟:

HashMap的長(zhǎng)度為什么是2的冪次方
為了能讓HashMap存取高效,盡量減少碰撞,也就是盡量把數(shù)據(jù)分配均勻,我們上面也講到了Hash值的范圍-2147483648 到 2147483647,前后加起來40億的映射空間,只要哈希函數(shù)映射的比較均勻松散,一般應(yīng)該是很難出現(xiàn)碰撞的,但是問題是一個(gè)40億長(zhǎng)度的數(shù)組內(nèi)存是放不下的。所以這個(gè)散列值不能直接拿來用。用之前還要對(duì)數(shù)組的長(zhǎng)度取模運(yùn)算,得到的余數(shù)才能用來要存放的位置也就是對(duì)應(yīng)的數(shù)組下標(biāo)。這個(gè)數(shù)組下標(biāo)的計(jì)算公式是(n-1)&hash。這也就解釋了為什么HashMap的長(zhǎng)度為什么是2的冪次方。
這里有一個(gè)公式:在length是2的n次方的時(shí)候:hash%length==hash&(length-1)
這個(gè)公式成立。
我們上面說的對(duì)數(shù)組長(zhǎng)度取模,之所以能演變成(n-1)&hash也是為了讓這個(gè)公式成立。因?yàn)檫@樣二進(jìn)制位操作比用%效率要高的多。
HashMap多線程操作死鎖問題
主要原因在于并發(fā)下的Rehash會(huì)造成元素之間形成一個(gè)循環(huán)鏈表。不過JDK1.8后解決了這個(gè)問題,但是還是不建議多線程的情況下使用HashMap,因?yàn)槎嗑€程下使用還會(huì)有其他問題,比如數(shù)據(jù)丟失等,并發(fā)環(huán)境建議使用ConcurrentHashMap.
HashMap有哪幾種常見的遍歷方式、
從大方向來說有四種:
- 迭代器方式遍歷(Iterator)
- For Each方式遍歷
- Lambda表達(dá)式遍歷
- streams API遍歷
但是美中類型又有不同的實(shí)現(xiàn),所以一共有七中遍歷方式:
- 使用迭代器(Iterator)EntrySet 的方式進(jìn)行遍歷;
- 使用迭代器(Iterator)KeySet 的方式進(jìn)行遍歷;
- 使用 For Each EntrySet 的方式進(jìn)行遍歷;
- 使用 For Each KeySet 的方式進(jìn)行遍歷;
- 使用 Lambda 表達(dá)式的方式進(jìn)行遍歷;
- 使用 Streams API 單線程的方式進(jìn)行遍歷;
- 使用 Streams API 多線程的方式進(jìn)行遍歷。
下面是簡(jiǎn)單是使用代碼:
public static void main(String[] args) {
Map<Integer,String> map = new HashMap<>();
map.put(1,"f");
map.put(2,"s");
map.put(3,"t");
// 第一種遍歷方式entrySet
Iterator<Map.Entry<Integer, String>> iterator = map.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry<Integer, String> entry = iterator.next();
System.out.println(entry.getKey()+":"+entry.getValue());
}
//第二種遍歷方式keySet
Iterator<Integer> iterator1 = map.keySet().iterator();
while (iterator1.hasNext()) {
Integer key = iterator1.next();
System.out.println(key+":"+map.get(key));
}
// 第三種遍歷方式For Each EntrySet
for (Map.Entry<Integer, String> entry : map.entrySet()) {
System.out.println(entry.getKey()+":"+entry.getValue());
}
//第四種遍歷方式For Each keySet
for (Integer key : map.keySet()) {
System.out.println(key+":"+map.get(key));
}
// 第五種遍歷方式lambda
map.forEach((key, value) -> {
System.out.println(key+":"+value);
});
// 第六種遍歷方式Streams API 單線程
map.entrySet().stream().forEach((entry) -> {
System.out.println(entry.getKey()+":"+entry.getValue());
});
// 第七種遍歷方式Streams API 多線程
map.entrySet().parallelStream().forEach((entry) -> {
System.out.println(entry.getKey()+":"+entry.getValue());
});
}
ConcurrentHashMap和Hashtable的區(qū)別
二者的主要區(qū)別體現(xiàn)在實(shí)現(xiàn)線程安全的方式不同
- 底層數(shù)據(jù)結(jié)構(gòu):jdk1.7的ConcurrentHashMap底層采用分段的數(shù)組+鏈表實(shí)現(xiàn),JDK1.8采用的數(shù)據(jù)結(jié)構(gòu)和HashMap一樣,數(shù)組+鏈表/紅黑樹.Hashtable1.8之前和HashMap的底層數(shù)據(jù)結(jié)構(gòu)類似都是數(shù)組+鏈表。
-
實(shí)現(xiàn)線程安全的方式:在JDK1.7的時(shí)候,ConcurrentHashMap(分段鎖)對(duì)整個(gè)桶數(shù)組進(jìn)行了分割分段,每一把鎖只鎖容器中一部分?jǐn)?shù)據(jù),多線程訪問容器中不同數(shù)據(jù)段的數(shù)據(jù)就不會(huì)存在競(jìng)爭(zhēng)。提高并發(fā)訪問率。到了JDK1.8已經(jīng)摒棄了分割分段的概念。直接用Node數(shù)組+鏈表+紅黑樹的數(shù)據(jù)結(jié)構(gòu)來實(shí)現(xiàn),并發(fā)控制使用synchronized和CAS來操作。整個(gè)看起來就像是優(yōu)化過且線程安全的HashMap。
Hashtable(同一把鎖)使用synchronized來保證線程安全,效率非常低,當(dāng)一個(gè)線程訪問同步方法時(shí),其他線程也訪問同步方法可能進(jìn)入阻塞或者輪詢狀態(tài)。如使用put添加元素,另一個(gè)線程不能使用put,而且也不能使用get,競(jìng)爭(zhēng)越激烈效率越低。
ConcurrentHashMap線程安全的具體實(shí)現(xiàn)方式/底層具體實(shí)現(xiàn)
JDK1.7
JDK1.7的時(shí)候?qū)?shù)據(jù)分為一段一段存儲(chǔ),然后給每一段數(shù)據(jù)配一把鎖。當(dāng)一個(gè)線程占用鎖訪問其中一個(gè)段數(shù)據(jù)時(shí),其他段的數(shù)據(jù)也能被其他線程訪問

ConcurrentHashMap是由Segment數(shù)組結(jié)構(gòu)和HashEntry數(shù)組結(jié)構(gòu)組成。
Segment繼承了ReentrantLock,所以Segment是一種可重入鎖,扮演鎖的角色,HashMap用于存儲(chǔ)鍵值對(duì)數(shù)據(jù)。
一個(gè)ConcurrentHashMap里包含一個(gè)Segment數(shù)組,Segment的結(jié)構(gòu)和HashMap類似,是一種數(shù)組和鏈表結(jié)構(gòu)。一個(gè)Segment包含一個(gè)HashEntry數(shù)組,每個(gè)HashEntry是一個(gè)鏈表結(jié)構(gòu)的元素,每個(gè)Segment守護(hù)一個(gè)HashEntry數(shù)組里的元素,當(dāng)對(duì)HashEntry數(shù)組的數(shù)據(jù)進(jìn)行修改時(shí),必須先獲取對(duì)應(yīng)的Segment的鎖。
JDK1.8
ConcurrentHashMap取消了Segment分段鎖,采用CAS和synchronized來保證并發(fā)安全。數(shù)據(jù)結(jié)構(gòu)和HashMap1.8的結(jié)構(gòu)類似。數(shù)組+鏈表/紅黑樹。java8在鏈表長(zhǎng)度超過一個(gè)閾值(8)時(shí)將鏈表轉(zhuǎn)化成紅黑樹。
synchronized只鎖定當(dāng)前鏈表或者紅黑樹的首節(jié)點(diǎn)。這樣只要hash不沖突,就不會(huì)產(chǎn)生并發(fā),效率大大的提升了。
Collections工具類
Collections工具類常用方法:
- 排序
- 查找替換操作
- 同步控制
排序操作
void reverse(List list)//反轉(zhuǎn)
void shuffle(List list)//隨機(jī)排序
void sort(List list)//按自然排序的升序排序
void sort(List list, Comparator c)//定制排序,由Comparator控制排序邏輯
void swap(List list, int i , int j)//交換兩個(gè)索引位置的元素
void rotate(List list, int distance)//旋轉(zhuǎn)。當(dāng)distance為正數(shù)時(shí),將list后distance個(gè)元素整體移到前面。當(dāng)distance為負(fù)數(shù)時(shí),將 list的前distance個(gè)元素整體移到后面
查找,替換操作
int binarySearch(List list, Object key)//對(duì)List進(jìn)行二分查找,返回索引,注意List必須是有序的
int max(Collection coll)//根據(jù)元素的自然順序,返回最大的元素。 類比int min(Collection coll)
int max(Collection coll, Comparator c)//根據(jù)定制排序,返回最大元素,排序規(guī)則由Comparatator類控制。類比int min(Collection coll, Comparator c)
void fill(List list, Object obj)//用指定的元素代替指定list中的所有元素
int frequency(Collection c, Object o)//統(tǒng)計(jì)元素出現(xiàn)次數(shù)
int indexOfSubList(List list, List target)//統(tǒng)計(jì)target在list中第一次出現(xiàn)的索引,找不到則返回-1,類比int lastIndexOfSubList(List source, list target)
boolean replaceAll(List list, Object oldVal, Object newVal)//用新元素替換舊元素
同步控制
Collections提供了多個(gè)synchronizedXxx()方法,該方法可以將指定集合包裝成線程同步的集合,從而解決多線程并發(fā)訪問集合時(shí)的線程安全問題。
我們知道HsahSet,TreeSet,ArrayList,LinkedList,HashMap,TreeMap都是線程不安全的。Collections提供了多個(gè)靜態(tài)方法可以把他們包裝秤線程安全的集合。
synchronizedCollection(Collection<T> c) //返回指定 collection 支持的同步(線程安全的)collection。
synchronizedList(List<T> list)//返回指定列表支持的同步(線程安全的)List。
synchronizedMap(Map<K,V> m) //返回由指定映射支持的同步(線程安全的)Map。
synchronizedSet(Set<T> s) //返回指定 set 支持的同步(線程安全的)set。
不過最好不用下面這些方法,因?yàn)樾史浅5?,需要線程安全的集合類型時(shí)最好用JUC包下的并發(fā)集合。
本篇筆記就整理到這里,如果稍微幫到你了記得點(diǎn)個(gè)喜歡點(diǎn)個(gè)關(guān)注,也祝大家工作順順利利!