文章目錄
- 集合容器概述
- 什麼是集合
- 集合的特點
- 集合和數組的差別
- 使用集合架構的好處
- 常用的集合類有哪些?
- List,Set,Map三者的差別?List、Set、Map 是否繼承自 Collection 接口?List、Map、Set 三個接口存取元素時,各有什麼特點?
- 集合架構底層資料結構
- 哪些集合類是線程安全的?
- Java集合的快速失敗機制 “fail-fast”?
- 怎麼確定一個集合不能被修改?
- Collection接口
- List接口
- 疊代器 Iterator 是什麼?
- Iterator 怎麼使用?有什麼特點?
- 如何邊周遊邊移除 Collection 中的元素?
- Iterator 和 ListIterator 有什麼差別?
- 周遊一個 List 有哪些不同的方式?每種方法的實作原理是什麼?Java 中 List 周遊的最佳實踐是什麼?
- 說一下 ArrayList 的優缺點
- 如何實作數組和 List 之間的轉換?
- ArrayList 和 LinkedList 的差別是什麼?
- ArrayList 和 Vector 的差別是什麼?
- 插入資料時,ArrayList、LinkedList、Vector誰速度較快?闡述 ArrayList、Vector、LinkedList 的存儲性能和特性?
- 多線程場景下如何使用 ArrayList?
- 為什麼 ArrayList 的 elementData 加上 transient 修飾?
- List 和 Set 的差別
- Set接口
- 說一下 HashSet 的實作原理?
- HashSet如何檢查重複?HashSet是如何保證資料不可重複的?
- HashSet與HashMap的差別
- Queue
- BlockingQueue是什麼?
- 在 Queue 中 poll()和 remove()有什麼差別?
- List接口
- Map接口
- 說一下 HashMap 的實作原理?
- HashMap在JDK1.7和JDK1.8中有哪些不同?HashMap的底層實作
- JDK1.8之前
- JDK1.8之後
- JDK1.7 VS JDK1.8 比較
- HashMap的put方法的具體流程?
- HashMap的擴容操作是怎麼實作的?
- HashMap是怎麼解決哈希沖突的?
- 什麼是哈希?
- 什麼是哈希沖突?
- HashMap的資料結構
- hash()函數
- JDK1.8新增紅黑樹
- 總結
- 能否使用任何類作為 Map 的 key?
- 為什麼HashMap中String、Integer這樣的包裝類适合作為K?
- 如果使用Object作為HashMap的Key,應該怎麼辦呢?
- HashMap為什麼不直接使用hashCode()處理後的哈希值直接作為table的下标?
- HashMap 的長度為什麼是2的幂次方
- HashMap 與 HashTable 有什麼差別?
- 如何決定使用 HashMap 還是 TreeMap?
- HashMap 和 ConcurrentHashMap 的差別
- ConcurrentHashMap 和 Hashtable 的差別?
- ConcurrentHashMap 底層具體實作知道嗎?實作原理是什麼?
- 輔助工具類
- Array 和 ArrayList 有何差別?
- 如何實作 Array 和 List 之間的轉換?
- comparable 和 comparator的差別?
- Collection 和 Collections 有什麼差別?
- TreeMap 和 TreeSet 在排序時如何比較元素?Collections 工具類中的 sort()方法如何比較元素?
Java面試總結彙總,整理了包括Java基礎知識,集合容器,并發程式設計,JVM,常用開源架構Spring,MyBatis,資料庫,中間件等,包含了作為一個Java工程師在面試中需要用到或者可能用到的絕大部分知識。歡迎大家閱讀,本人見識有限,寫的部落格難免有錯誤或者疏忽的地方,還望各位大佬指點,在此表示感激不盡。文章持續更新中…
序号 | 内容 | 連結位址 |
---|---|---|
1 | Java基礎知識面試題(2020最新版) | https://thinkwon.blog.csdn.net/article/details/104390612 |
2 | Java集合容器面試題(2020最新版) | https://thinkwon.blog.csdn.net/article/details/104588551 |
3 | Java異常面試題(2020最新版) | https://thinkwon.blog.csdn.net/article/details/104390689 |
4 | 并發程式設計面試題(2020最新版) | https://thinkwon.blog.csdn.net/article/details/104863992 |
5 | JVM面試題(2020最新版) | https://thinkwon.blog.csdn.net/article/details/104390752 |
6 | Spring面試題(2020最新版) | https://thinkwon.blog.csdn.net/article/details/104397516 |
7 | Spring MVC面試題(2020最新版) | https://thinkwon.blog.csdn.net/article/details/104397427 |
8 | Spring Boot面試題(2020最新版) | https://thinkwon.blog.csdn.net/article/details/104397299 |
9 | Spring Cloud面試題(2020最新版) | https://thinkwon.blog.csdn.net/article/details/104397367 |
10 | MyBatis面試題(2020最新版) | https://thinkwon.blog.csdn.net/article/details/101292950 |
11 | Redis面試題(2020最新版) | https://thinkwon.blog.csdn.net/article/details/103522351 |
12 | MySQL資料庫面試題(2020最新版) | https://thinkwon.blog.csdn.net/article/details/104778621 |
13 | 消息中間件MQ與RabbitMQ面試題(2020最新版) | https://thinkwon.blog.csdn.net/article/details/104588612 |
14 | Dubbo面試題(2020最新版) | https://thinkwon.blog.csdn.net/article/details/104390006 |
15 | Linux面試題(2020最新版) | https://thinkwon.blog.csdn.net/article/details/104588679 |
16 | Tomcat面試題(2020最新版) | https://thinkwon.blog.csdn.net/article/details/104397665 |
17 | ZooKeeper面試題(2020最新版) | https://thinkwon.blog.csdn.net/article/details/104397719 |
18 | Netty面試題(2020最新版) | https://thinkwon.blog.csdn.net/article/details/104391081 |
集合容器概述
什麼是集合
集合架構:用于存儲資料的容器。
集合架構是為表示和操作集合而規定的一種統一的标準的體系結構。
任何集合架構都包含三大塊内容:對外的接口、接口的實作和對集合運算的算法。
接口:表示集合的抽象資料類型。接口允許我們操作集合時不必關注具體實作,進而達到“多态”。在面向對象程式設計語言中,接口通常用來形成規範。
實作:集合接口的具體實作,是重用性很高的資料結構。
算法:在一個實作了某個集合架構中的接口的對象身上完成某種有用的計算的方法,例如查找、排序等。這些算法通常是多态的,因為相同的方法可以在同一個接口被多個類實作時有不同的表現。事實上,算法是可複用的函數。
它減少了程式設計的辛勞。
集合架構通過提供有用的資料結構和算法使你能集中注意力于你的程式的重要部分上,而不是為了讓程式能正常運轉而将注意力于低層設計上。
通過這些在無關API之間的簡易的互用性,使你免除了為改編對象或轉換代碼以便聯合這些API而去寫大量的代碼。 它提高了程式速度和品質。
集合的特點
集合的特點主要有如下兩點:
- 對象封裝資料,對象多了也需要存儲。集合用于存儲對象。
- 對象的個數确定可以使用數組,對象的個數不确定的可以用集合。因為集合是可變長度的。
集合和數組的差別
- 數組是固定長度的;集合可變長度的。
- 數組可以存儲基本資料類型,也可以存儲引用資料類型;集合隻能存儲引用資料類型。
- 數組存儲的元素必須是同一個資料類型;集合存儲的對象可以是不同資料類型。
資料結構:就是容器中存儲資料的方式。
對于集合容器,有很多種。因為每一個容器的自身特點不同,其實原理在于每個容器的内部資料結構不同。
集合容器在不斷向上抽取過程中,出現了集合體系。在使用一個體系的原則:參閱頂層内容。建立底層對象。
使用集合架構的好處
- 容量自增長;
- 提供了高性能的資料結構和算法,使編碼更輕松,提高了程式速度和品質;
- 允許不同 API 之間的互操作,API之間可以來回傳遞集合;
- 可以友善地擴充或改寫集合,提高代碼複用性和可操作性。
- 通過使用JDK自帶的集合類,可以降低代碼維護和學習新API成本。
常用的集合類有哪些?
Map接口和Collection接口是所有集合架構的父接口:
- Collection接口的子接口包括:Set接口和List接口
- Map接口的實作類主要有:HashMap、TreeMap、Hashtable、ConcurrentHashMap以及Properties等
- Set接口的實作類主要有:HashSet、TreeSet、LinkedHashSet等
- List接口的實作類主要有:ArrayList、LinkedList、Stack以及Vector等
List,Set,Map三者的差別?List、Set、Map 是否繼承自 Collection 接口?List、Map、Set 三個接口存取元素時,各有什麼特點?

Java 容器分為 Collection 和 Map 兩大類,Collection集合的子接口有Set、List、Queue三種子接口。我們比較常用的是Set、List,Map接口不是collection的子接口。
Collection集合主要有List和Set兩大接口
- List:一個有序(元素存入集合的順序和取出的順序一緻)容器,元素可以重複,可以插入多個null元素,元素都有索引。常用的實作類有 ArrayList、LinkedList 和 Vector。
- Set:一個無序(存入和取出順序有可能不一緻)容器,不可以存儲重複元素,隻允許存入一個null元素,必須保證元素唯一性。Set 接口常用實作類是 HashSet、LinkedHashSet 以及 TreeSet。
Map是一個鍵值對集合,存儲鍵、值和之間的映射。 Key無序,唯一;value 不要求有序,允許重複。Map沒有繼承于Collection接口,從Map集合中檢索元素時,隻要給出鍵對象,就會傳回對應的值對象。
Map 的常用實作類:HashMap、TreeMap、HashTable、LinkedHashMap、ConcurrentHashMap
集合架構底層資料結構
Collection
- List
- Arraylist: Object數組
- Vector: Object數組
- LinkedList: 雙向循環連結清單
- Set
- HashSet(無序,唯一):基于 HashMap 實作的,底層采用 HashMap 來儲存元素
- LinkedHashSet: LinkedHashSet 繼承與 HashSet,并且其内部是通過 LinkedHashMap 來實作的。有點類似于我們之前說的LinkedHashMap 其内部是基于 Hashmap 實作一樣,不過還是有一點點差別的。
- TreeSet(有序,唯一): 紅黑樹(自平衡的排序二叉樹。)
Map
- HashMap: JDK1.8之前HashMap由數組+連結清單組成的,數組是HashMap的主體,連結清單則是主要為了解決哈希沖突而存在的(“拉鍊法”解決沖突).JDK1.8以後在解決哈希沖突時有了較大的變化,當連結清單長度大于門檻值(預設為8)時,将連結清單轉化為紅黑樹,以減少搜尋時間
- LinkedHashMap:LinkedHashMap 繼承自 HashMap,是以它的底層仍然是基于拉鍊式散列結構即由數組和連結清單或紅黑樹組成。另外,LinkedHashMap 在上面結構的基礎上,增加了一條雙向連結清單,使得上面的結構可以保持鍵值對的插入順序。同時通過對連結清單進行相應的操作,實作了通路順序相關邏輯。
- HashTable: 數組+連結清單組成的,數組是 HashMap 的主體,連結清單則是主要為了解決哈希沖突而存在的
- TreeMap: 紅黑樹(自平衡的排序二叉樹)
哪些集合類是線程安全的?
- vector:就比arraylist多了個同步化機制(線程安全),因為效率較低,現在已經不太建議使用。在web應用中,特别是前台頁面,往往效率(頁面響應速度)是優先考慮的。
- statck:堆棧類,先進後出。
- hashtable:就比hashmap多了個線程安全。
- enumeration:枚舉,相當于疊代器。
Java集合的快速失敗機制 “fail-fast”?
是java集合的一種錯誤檢測機制,當多個線程對集合進行結構上的改變的操作時,有可能會産生 fail-fast 機制。
例如:假設存在兩個線程(線程1、線程2),線程1通過Iterator在周遊集合A中的元素,在某個時候線程2修改了集合A的結構(是結構上面的修改,而不是簡單的修改集合元素的内容),那麼這個時候程式就會抛出 ConcurrentModificationException 異常,進而産生fail-fast機制。
原因:疊代器在周遊時直接通路集合中的内容,并且在周遊過程中使用一個 modCount 變量。集合在被周遊期間如果内容發生變化,就會改變modCount的值。每當疊代器使用hashNext()/next()周遊下一個元素之前,都會檢測modCount變量是否為expectedmodCount值,是的話就傳回周遊;否則抛出異常,終止周遊。
解決辦法:
- 在周遊過程中,所有涉及到改變modCount值得地方全部加上synchronized。
- 使用CopyOnWriteArrayList來替換ArrayList
怎麼確定一個集合不能被修改?
可以使用 Collections. unmodifiableCollection(Collection c) 方法來建立一個隻讀集合,這樣改變集合的任何操作都會抛出 Java. lang. UnsupportedOperationException 異常。
示例代碼如下:
List<String> list = new ArrayList<>();
list. add("x");
Collection<String> clist = Collections. unmodifiableCollection(list);
clist. add("y"); // 運作時此行報錯
System. out. println(list. size());
複制
Collection接口
List接口
疊代器 Iterator 是什麼?
Iterator 接口提供周遊任何 Collection 的接口。我們可以從一個 Collection 中使用疊代器方法來擷取疊代器執行個體。疊代器取代了 Java 集合架構中的 Enumeration,疊代器允許調用者在疊代過程中移除元素。
Iterator 怎麼使用?有什麼特點?
Iterator 使用代碼如下:
List<String> list = new ArrayList<>();
Iterator<String> it = list. iterator();
while(it. hasNext()){
String obj = it. next();
System. out. println(obj);
}
複制
Iterator 的特點是隻能單向周遊,但是更加安全,因為它可以確定,在目前周遊的集合元素被更改的時候,就會抛出 ConcurrentModificationException 異常。
如何邊周遊邊移除 Collection 中的元素?
邊周遊邊修改 Collection 的唯一正确方式是使用 Iterator.remove() 方法,如下:
Iterator<Integer> it = list.iterator();
while(it.hasNext()){
*// do something*
it.remove();
}
複制
一種最常見的錯誤代碼如下:
for(Integer i : list){
list.remove(i)
}
複制
運作以上錯誤代碼會報 ConcurrentModificationException 異常。這是因為當使用 foreach(for(Integer i : list)) 語句時,會自動生成一個iterator 來周遊該 list,但同時該 list 正在被 Iterator.remove() 修改。Java 一般不允許一個線程在周遊 Collection 時另一個線程修改它。
Iterator 和 ListIterator 有什麼差別?
- Iterator 可以周遊 Set 和 List 集合,而 ListIterator 隻能周遊 List。
- Iterator 隻能單向周遊,而 ListIterator 可以雙向周遊(向前/後周遊)。
- ListIterator 實作 Iterator 接口,然後添加了一些額外的功能,比如添加一個元素、替換一個元素、擷取前面或後面元素的索引位置。
周遊一個 List 有哪些不同的方式?每種方法的實作原理是什麼?Java 中 List 周遊的最佳實踐是什麼?
周遊方式有以下幾種:
- for 循環周遊,基于計數器。在集合外部維護一個計數器,然後依次讀取每一個位置的元素,當讀取到最後一個元素後停止。
- 疊代器周遊,Iterator。Iterator 是面向對象的一個設計模式,目的是屏蔽不同資料集合的特點,統一周遊集合的接口。Java 在 Collections 中支援了 Iterator 模式。
- foreach 循環周遊。foreach 内部也是采用了 Iterator 的方式實作,使用時不需要顯式聲明 Iterator 或計數器。優點是代碼簡潔,不易出錯;缺點是隻能做簡單的周遊,不能在周遊過程中操作資料集合,例如删除、替換。
最佳實踐:Java Collections 架構中提供了一個 RandomAccess 接口,用來标記 List 實作是否支援 Random Access。
- 如果一個資料集合實作了該接口,就意味着它支援 Random Access,按位置讀取元素的平均時間複雜度為 O(1),如ArrayList。
- 如果沒有實作該接口,表示不支援 Random Access,如LinkedList。
推薦的做法就是,支援 Random Access 的清單可用 for 循環周遊,否則建議用 Iterator 或 foreach 周遊。
說一下 ArrayList 的優缺點
ArrayList的優點如下:
- ArrayList 底層以數組實作,是一種随機通路模式。ArrayList 實作了 RandomAccess 接口,是以查找的時候非常快。
- ArrayList 在順序添加一個元素的時候非常友善。
ArrayList 的缺點如下:
- 删除元素的時候,需要做一次元素複制操作。如果要複制的元素很多,那麼就會比較耗費性能。
- 插入元素的時候,也需要做一次元素複制操作,缺點同上。
ArrayList 比較适合順序添加、随機通路的場景。
如何實作數組和 List 之間的轉換?
- 數組轉 List:使用 Arrays. asList(array) 進行轉換。
- List 轉數組:使用 List 自帶的 toArray() 方法。
代碼示例:
// list to array
List<String> list = new ArrayList<String>();
list.add("123");
list.add("456");
list.toArray();
// array to list
String[] array = new String[]{"123","456"};
Arrays.asList(array);
複制
ArrayList 和 LinkedList 的差別是什麼?
- 資料結構實作:ArrayList 是動态數組的資料結構實作,而 LinkedList 是雙向連結清單的資料結構實作。
- 随機通路效率:ArrayList 比 LinkedList 在随機通路的時候效率要高,因為 LinkedList 是線性的資料存儲方式,是以需要移動指針從前往後依次查找。
- 增加和删除效率:在非首尾的增加和删除操作,LinkedList 要比 ArrayList 效率要高,因為 ArrayList 增删操作要影響數組内的其他資料的下标。
- 記憶體空間占用:LinkedList 比 ArrayList 更占記憶體,因為 LinkedList 的節點除了存儲資料,還存儲了兩個引用,一個指向前一個元素,一個指向後一個元素。
- 線程安全:ArrayList 和 LinkedList 都是不同步的,也就是不保證線程安全;
綜合來說,在需要頻繁讀取集合中的元素時,更推薦使用 ArrayList,而在插入和删除操作較多時,更推薦使用 LinkedList。
補充:資料結構基礎之雙向連結清單
雙向連結清單也叫雙連結清單,是連結清單的一種,它的每個資料結點中都有兩個指針,分别指向直接後繼和直接前驅。是以,從雙向連結清單中的任意一個結點開始,都可以很友善地通路它的前驅結點和後繼結點。
ArrayList 和 Vector 的差別是什麼?
這兩個類都實作了 List 接口(List 接口繼承了 Collection 接口),他們都是有序集合
- 線程安全:Vector 使用了 Synchronized 來實作線程同步,是線程安全的,而 ArrayList 是非線程安全的。
- 性能:ArrayList 在性能方面要優于 Vector。
- 擴容:ArrayList 和 Vector 都會根據實際的需要動态的調整容量,隻不過在 Vector 擴容每次會增加 1 倍,而 ArrayList 隻會增加 50%。
Vector類的所有方法都是同步的。可以由兩個線程安全地通路一個Vector對象、但是一個線程通路Vector的話代碼要在同步操作上耗費大量的時間。
Arraylist不是同步的,是以在不需要保證線程安全時時建議使用Arraylist。
插入資料時,ArrayList、LinkedList、Vector誰速度較快?闡述 ArrayList、Vector、LinkedList 的存儲性能和特性?
ArrayList、LinkedList、Vector 底層的實作都是使用數組方式存儲資料。數組元素數大于實際存儲的資料以便增加和插入元素,它們都允許直接按序号索引元素,但是插入元素要涉及數組元素移動等記憶體操作,是以索引資料快而插入資料慢。
Vector 中的方法由于加了 synchronized 修飾,是以 Vector 是線程安全容器,但性能上較ArrayList差。
LinkedList 使用雙向連結清單實作存儲,按序号索引資料需要進行前向或後向周遊,但插入資料時隻需要記錄目前項的前後項即可,是以 LinkedList 插入速度較快。
多線程場景下如何使用 ArrayList?
ArrayList 不是線程安全的,如果遇到多線程場景,可以通過 Collections 的 synchronizedList 方法将其轉換成線程安全的容器後再使用。例如像下面這樣:
List<String> synchronizedList = Collections.synchronizedList(list);
synchronizedList.add("aaa");
synchronizedList.add("bbb");
for (int i = 0; i < synchronizedList.size(); i++) {
System.out.println(synchronizedList.get(i));
}
複制
為什麼 ArrayList 的 elementData 加上 transient 修飾?
ArrayList 中的數組定義如下:
private transient Object[] elementData;
複制
再看一下 ArrayList 的定義:
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
複制
可以看到 ArrayList 實作了 Serializable 接口,這意味着 ArrayList 支援序列化。transient 的作用是說不希望 elementData 數組被序列化,重寫了 writeObject 實作:
private void writeObject(java.io.ObjectOutputStream s) throws java.io.IOException{
*// Write out element count, and any hidden stuff*
int expectedModCount = modCount;
s.defaultWriteObject();
*// Write out array length*
s.writeInt(elementData.length);
*// Write out all elements in the proper order.*
for (int i=0; i<size; i++)
s.writeObject(elementData[i]);
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
複制
每次序列化時,先調用 defaultWriteObject() 方法序列化 ArrayList 中的非 transient 元素,然後周遊 elementData,隻序列化已存入的元素,這樣既加快了序列化的速度,又減小了序列化之後的檔案大小。
List 和 Set 的差別
List , Set 都是繼承自Collection 接口
List 特點:一個有序(元素存入集合的順序和取出的順序一緻)容器,元素可以重複,可以插入多個null元素,元素都有索引。常用的實作類有 ArrayList、LinkedList 和 Vector。
Set 特點:一個無序(存入和取出順序有可能不一緻)容器,不可以存儲重複元素,隻允許存入一個null元素,必須保證元素唯一性。Set 接口常用實作類是 HashSet、LinkedHashSet 以及 TreeSet。
另外 List 支援for循環,也就是通過下标來周遊,也可以用疊代器,但是set隻能用疊代,因為他無序,無法用下标來取得想要的值。
Set和List對比
Set:檢索元素效率低下,删除和插入效率高,插入和删除不會引起元素位置改變。
List:和數組類似,List可以動态增長,查找元素效率高,插入删除元素效率低,因為會引起其他元素位置改變
Set接口
說一下 HashSet 的實作原理?
HashSet 是基于 HashMap 實作的,HashSet的值存放于HashMap的key上,HashMap的value統一為PRESENT,是以 HashSet 的實作比較簡單,相關 HashSet 的操作,基本上都是直接調用底層 HashMap 的相關方法來完成,HashSet 不允許重複的值。
HashSet如何檢查重複?HashSet是如何保證資料不可重複的?
向HashSet 中add ()元素時,判斷元素是否存在的依據,不僅要比較hash值,同時還要結合equles 方法比較。
HashSet 中的add ()方法會使用HashMap 的put()方法。
HashMap 的 key 是唯一的,由源碼可以看出 HashSet 添加進去的值就是作為HashMap 的key,并且在HashMap中如果K/V相同時,會用新的V覆寫掉舊的V,然後傳回舊的V。是以不會重複( HashMap 比較key是否相等是先比較hashcode 再比較equals )。
以下是HashSet 部分源碼:
private static final Object PRESENT = new Object();
private transient HashMap<E,Object> map;
public HashSet() {
map = new HashMap<>();
}
public boolean add(E e) {
// 調用HashMap的put方法,PRESENT是一個至始至終都相同的虛值
return map.put(e, PRESENT)==null;
}
複制
hashCode()與equals()的相關規定:
- 如果兩個對象相等,則hashcode一定也是相同的
- 兩個對象相等,對兩個equals方法傳回true
- 兩個對象有相同的hashcode值,它們也不一定是相等的
- 綜上,equals方法被覆寫過,則hashCode方法也必須被覆寫
- hashCode()的預設行為是對堆上的對象産生獨特值。如果沒有重寫hashCode(),則該class的兩個對象無論如何都不會相等(即使這兩個對象指向相同的資料)。
==與equals的差別
- ==是判斷兩個變量或執行個體是不是指向同一個記憶體空間 equals是判斷兩個變量或執行個體所指向的記憶體空間的值是不是相同
- ==是指對記憶體位址進行比較 equals()是對字元串的内容進行比較3.==指引用是否相同 equals()指的是值是否相同
HashSet與HashMap的差別
HashMap | HashSet |
---|---|
實作了Map接口 | 實作Set接口 |
存儲鍵值對 | 僅存儲對象 |
調用put()向map中添加元素 | 調用add()方法向Set中添加元素 |
HashMap使用鍵(Key)計算Hashcode | HashSet使用成員對象來計算hashcode值,對于兩個對象來說hashcode可能相同,是以equals()方法用來判斷對象的相等性,如果兩個對象不同的話,那麼傳回false |
HashMap相對于HashSet較快,因為它是使用唯一的鍵擷取對象 | HashSet較HashMap來說比較慢 |
Queue
BlockingQueue是什麼?
Java.util.concurrent.BlockingQueue是一個隊列,在進行檢索或移除一個元素的時候,它會等待隊列變為非空;當在添加一個元素時,它會等待隊列中的可用空間。BlockingQueue接口是Java集合架構的一部分,主要用于實作生産者-消費者模式。我們不需要擔心等待生産者有可用的空間,或消費者有可用的對象,因為它都在BlockingQueue的實作類中被處理了。Java提供了集中BlockingQueue的實作,比如ArrayBlockingQueue、LinkedBlockingQueue、PriorityBlockingQueue,、SynchronousQueue等。
在 Queue 中 poll()和 remove()有什麼差別?
- 相同點:都是傳回第一個元素,并在隊列中删除傳回的對象。
- 不同點:如果沒有元素 poll()會傳回 null,而 remove()會直接抛出 NoSuchElementException 異常。
代碼示例:
Queue<String> queue = new LinkedList<String>();
queue. offer("string"); // add
System. out. println(queue. poll());
System. out. println(queue. remove());
System. out. println(queue. size());
複制
Map接口
說一下 HashMap 的實作原理?
HashMap概述: HashMap是基于哈希表的Map接口的非同步實作。此實作提供所有可選的映射操作,并允許使用null值和null鍵。此類不保證映射的順序,特别是它不保證該順序恒久不變。
HashMap的資料結構: 在Java程式設計語言中,最基本的結構就是兩種,一個是數組,另外一個是模拟指針(引用),所有的資料結構都可以用這兩個基本結構來構造的,HashMap也不例外。HashMap實際上是一個“連結清單散列”的資料結構,即數組和連結清單的結合體。
HashMap 基于 Hash 算法實作的
- 當我們往Hashmap中put元素時,利用key的hashCode重新hash計算出目前對象的元素在數組中的下标
- 存儲時,如果出現hash值相同的key,此時有兩種情況。(1)如果key相同,則覆寫原始值;(2)如果key不同(出現沖突),則将目前的key-value放傳入連結表中
- 擷取時,直接找到hash值對應的下标,在進一步判斷key是否相同,進而找到對應值。
- 了解了以上過程就不難明白HashMap是如何解決hash沖突的問題,核心就是使用了數組的存儲方式,然後将沖突的key的對象放傳入連結表中,一旦發現沖突就在連結清單中做進一步的對比。
需要注意Jdk 1.8中對HashMap的實作做了優化,當連結清單中的節點資料超過八個之後,該連結清單會轉為紅黑樹來提高查詢效率,從原來的O(n)到O(logn)
HashMap在JDK1.7和JDK1.8中有哪些不同?HashMap的底層實作
在Java中,儲存資料有兩種比較簡單的資料結構:數組和連結清單。數組的特點是:尋址容易,插入和删除困難;連結清單的特點是:尋址困難,但插入和删除容易;是以我們将數組和連結清單結合在一起,發揮兩者各自的優勢,使用一種叫做拉鍊法的方式可以解決哈希沖突。
JDK1.8之前
JDK1.8之前采用的是拉鍊法。拉鍊法:将連結清單和數組相結合。也就是說建立一個連結清單數組,數組中每一格就是一個連結清單。若遇到哈希沖突,則将沖突的值加到連結清單中即可。
JDK1.8之後
相比于之前的版本,jdk1.8在解決哈希沖突時有了較大的變化,當連結清單長度大于門檻值(預設為8)時,将連結清單轉化為紅黑樹,以減少搜尋時間。
JDK1.7 VS JDK1.8 比較
JDK1.8主要解決或優化了一下問題:
- resize 擴容優化
- 引入了紅黑樹,目的是避免單條連結清單過長而影響查詢效率,紅黑樹算法請參考
- 解決了多線程死循環問題,但仍是非線程安全的,多線程時可能會造成資料丢失問題。
不同 | JDK 1.7 | JDK 1.8 |
---|---|---|
存儲結構 | 數組 + 連結清單 | 數組 + 連結清單 + 紅黑樹 |
初始化方式 | 單獨函數:inflateTable() | 直接內建到了擴容函數resize()中 |
hash值計算方式 | 擾動處理 = 9次擾動 = 4次位運算 + 5次異或運算 | 擾動處理 = 2次擾動 = 1次位運算 + 1次異或運算 |
存放資料的規則 | 無沖突時,存放數組;沖突時,存放連結清單 | 無沖突時,存放數組;沖突 & 連結清單長度 < 8:存放單連結清單;沖突 & 連結清單長度 > 8:樹化并存放紅黑樹 |
插入資料方式 | 頭插法(先講原位置的資料移到後1位,再插入資料到該位置) | 尾插法(直接插入到連結清單尾部/紅黑樹) |
擴容後存儲位置的計算方式 | 全部按照原來方法進行計算(即hashCode ->> 擾動函數 ->> (h&length-1)) | 按照擴容後的規律計算(即擴容後的位置=原位置 or 原位置 + 舊容量) |
HashMap的put方法的具體流程?
當我們put的時候,首先計算
key
的
hash
值,這裡調用了
hash
方法,
hash
方法實際是讓
key.hashCode()
與
key.hashCode()>>>16
進行異或操作,高16bit補0,一個數和0異或不變,是以 hash 函數大概的作用就是:高16bit不變,低16bit和高16bit做了一個異或,目的是減少碰撞。按照函數注釋,因為bucket數組大小是2的幂,計算下标
index = (table.length - 1) & hash
,如果不做 hash 處理,相當于散列生效的隻有幾個低 bit 位,為了減少散列的碰撞,設計者綜合考慮了速度、作用、品質之後,使用高16bit和低16bit異或來簡單處理減少碰撞,而且JDK8中用了複雜度 O(logn)的樹結構來提升碰撞下的性能。
putVal方法執行流程圖
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
//實作Map.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為空則建立
// table未初始化或者長度為0,進行擴容
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// 步驟②:計算index,并對null做處理
// (n - 1) & hash 确定元素存放在哪個桶中,桶為空,新生成結點放入桶中(此時,這個結點是放在數組中)
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
// 桶中已經存在元素
else {
Node<K,V> e; K k;
// 步驟③:節點key存在,直接覆寫value
// 比較桶中第一個元素(數組中的結點)的hash值相等,key相等
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
// 将第一個元素指派給e,用e來記錄
e = p;
// 步驟④:判斷該鍊為紅黑樹
// hash值不相等,即key不相等;為紅黑樹結點
// 如果目前元素類型為TreeNode,表示為紅黑樹,putTreeVal傳回待存放的node, e可能為null
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值與插入的元素的key值是否相等
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
// 相等,跳出循環
break;
// 用于周遊桶中的連結清單,與前面的e = p.next組合,可以周遊連結清單
p = e;
}
}
//判斷目前的key已經存在的情況下,再來一個相同的hash值、key值時,傳回新來的value這個值
if (e != null) {
// 記錄e的value
V oldValue = e.value;
// onlyIfAbsent為false或者舊值為null
if (!onlyIfAbsent || oldValue == null)
//用新值替換舊值
e.value = value;
// 通路後回調
afterNodeAccess(e);
// 傳回舊值
return oldValue;
}
}
// 結構性修改
++modCount;
// 步驟⑥:超過最大容量就擴容
// 實際大小大于門檻值則擴容
if (++size > threshold)
resize();
// 插入後回調
afterNodeInsertion(evict);
return null;
}
複制
①.判斷鍵值對數組table[i]是否為空或為null,否則執行resize()進行擴容;
②.根據鍵值key計算hash值得到插入的數組索引i,如果table[i]==null,直接建立節點添加,轉向⑥,如果table[i]不為空,轉向③;
③.判斷table[i]的首個元素是否和key一樣,如果相同直接覆寫value,否則轉向④,這裡的相同指的是hashCode以及equals;
④.判斷table[i] 是否為treeNode,即table[i] 是否是紅黑樹,如果是紅黑樹,則直接在樹中插入鍵值對,否則轉向⑤;
⑤.周遊table[i],判斷連結清單長度是否大于8,大于8的話把連結清單轉換為紅黑樹,在紅黑樹中執行插入操作,否則進行連結清單的插入操作;周遊過程中若發現key已經存在直接覆寫value即可;
⑥.插入成功後,判斷實際存在的鍵值對數量size是否超多了最大容量threshold,如果超過,進行擴容。
HashMap的擴容操作是怎麼實作的?
①.在jdk1.8中,resize方法是在hashmap中的鍵值對大于閥值時或者初始化時,就調用resize方法進行擴容;
②.每次擴充的時候,都是擴充2倍;
③.擴充後Node對象的位置要麼在原位置,要麼移動到原偏移量兩倍的位置。
在putVal()中,我們看到在這個函數裡面使用到了2次resize()方法,resize()方法表示的在進行第一次初始化時會對其進行擴容,或者當該數組的實際大小大于其臨界值值(第一次為12),這個時候在擴容的同時也會伴随的桶上面的元素進行重新分發,這也是JDK1.8版本的一個優化的地方,在1.7中,擴容之後需要重新去計算其Hash值,根據Hash值對其進行分發,但在1.8版本中,則是根據在同一個桶的位置中進行判斷(e.hash & oldCap)是否為0,重新進行hash配置設定後,該元素的位置要麼停留在原始位置,要麼移動到原始位置+增加的數組大小這個位置上
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;//oldTab指向hash桶數組
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
if (oldCap > 0) {//如果oldCap不為空的話,就是hash桶數組不為空
if (oldCap >= MAXIMUM_CAPACITY) {//如果大于最大容量了,就指派為整數最大的閥值
threshold = Integer.MAX_VALUE;
return oldTab;//傳回
}//如果目前hash桶數組的長度在擴容後仍然小于最大容量 并且oldCap大于預設值16
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold 雙倍擴容閥值threshold
}
// 舊的容量為0,但threshold大于零,代表有參構造有cap傳入,threshold已經被初始化成最小2的n次幂
// 直接将該值賦給新的容量
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
// 無參構造建立的map,給出預設容量和threshold 16, 16*0.75
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
// 新的threshold = 新的cap * 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;
// 計算出新的數組長度後賦給目前成員變量table
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];//建立hash桶數組
table = newTab;//将新數組的值複制給舊的hash桶數組
// 如果原先的數組沒有初始化,那麼resize的初始化工作到此結束,否則進入擴容元素重排邏輯,使其均勻的分散
if (oldTab != null) {
// 周遊新數組的所有桶下标
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
// 舊數組的桶下标賦給臨時變量e,并且解除舊數組中的引用,否則就數組無法被GC回收
oldTab[j] = null;
// 如果e.next==null,代表桶中就一個元素,不存在連結清單或者紅黑樹
if (e.next == null)
// 用同樣的hash映射算法把該元素加入新的數組
newTab[e.hash & (newCap - 1)] = e;
// 如果e是TreeNode并且e.next!=null,那麼處理樹中元素的重排
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
// e是連結清單的頭并且e.next!=null,那麼處理連結清單中元素重排
else { // preserve order
// loHead,loTail 代表擴容後不用變換下标,見注1
Node<K,V> loHead = null, loTail = null;
// hiHead,hiTail 代表擴容後變換下标,見注1
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
// 周遊連結清單
do {
next = e.next;
if ((e.hash & oldCap) == 0) {
if (loTail == null)
// 初始化head指向連結清單目前元素e,e不一定是連結清單的第一個元素,初始化後loHead
// 代表下标保持不變的連結清單的頭元素
loHead = e;
else
// loTail.next指向目前e
loTail.next = e;
// loTail指向目前的元素e
// 初始化後,loTail和loHead指向相同的記憶體,是以當loTail.next指向下一個元素時,
// 底層數組中的元素的next引用也相應發生變化,造成lowHead.next.next.....
// 跟随loTail同步,使得lowHead可以連結到所有屬于該連結清單的元素。
loTail = e;
}
else {
if (hiTail == null)
// 初始化head指向連結清單目前元素e, 初始化後hiHead代表下标更改的連結清單頭元素
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
// 周遊結束, 将tail指向null,并把連結清單頭放入新數組的相應下标,形成新的映射。
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
複制
HashMap是怎麼解決哈希沖突的?
答:在解決這個問題之前,我們首先需要知道什麼是哈希沖突,而在了解哈希沖突之前我們還要知道什麼是哈希才行;
什麼是哈希?
Hash,一般翻譯為“散列”,也有直接音譯為“哈希”的,這就是把任意長度的輸入通過雜湊演算法,變換成固定長度的輸出,該輸出就是散列值(哈希值);這種轉換是一種壓縮映射,也就是,散列值的空間通常遠小于輸入的空間,不同的輸入可能會散列成相同的輸出,是以不可能從散列值來唯一的确定輸入值。簡單的說就是一種将任意長度的消息壓縮到某一固定長度的消息摘要的函數。
所有散列函數都有如下一個基本特性**:根據同一散列函數計算出的散列值如果不同,那麼輸入值肯定也不同。但是,根據同一散列函數計算出的散列值如果相同,輸入值不一定相同**。
什麼是哈希沖突?
當兩個不同的輸入值,根據同一散列函數計算出相同的散列值的現象,我們就把它叫做碰撞(哈希碰撞)。
HashMap的資料結構
在Java中,儲存資料有兩種比較簡單的資料結構:數組和連結清單。數組的特點是:尋址容易,插入和删除困難;連結清單的特點是:尋址困難,但插入和删除容易;是以我們将數組和連結清單結合在一起,發揮兩者各自的優勢,使用一種叫做鍊位址法的方式可以解決哈希沖突:
這樣我們就可以将擁有相同哈希值的對象組織成一個連結清單放在hash值所對應的bucket下,但相比于hashCode傳回的int類型,我們HashMap初始的容量大小
DEFAULT_INITIAL_CAPACITY = 1 << 4
(即2的四次方16)要遠小于int類型的範圍,是以我們如果隻是單純的用hashCode取餘來擷取對應的bucket這将會大大增加哈希碰撞的機率,并且最壞情況下還會将HashMap變成一個單連結清單,是以我們還需要對hashCode作一定的優化
hash()函數
上面提到的問題,主要是因為如果使用hashCode取餘,那麼相當于參與運算的隻有hashCode的低位,高位是沒有起到任何作用的,是以我們的思路就是讓hashCode取值出的高位也參與運算,進一步降低hash碰撞的機率,使得資料分布更平均,我們把這樣的操作稱為擾動,在JDK 1.8中的hash()函數如下:
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);// 與自己右移16位進行異或運算(高低位異或)
}
複制
這比在JDK 1.7中,更為簡潔,相比在1.7中的4次位運算,5次異或運算(9次擾動),在1.8中,隻進行了1次位運算和1次異或運算(2次擾動);
JDK1.8新增紅黑樹
通過上面的鍊位址法(使用散清單)和擾動函數我們成功讓我們的資料分布更平均,哈希碰撞減少,但是當我們的HashMap中存在大量資料時,加入我們某個bucket下對應的連結清單有n個元素,那麼周遊時間複雜度就為O(n),為了針對這個問題,JDK1.8在HashMap中新增了紅黑樹的資料結構,進一步使得周遊複雜度降低至O(logn);
總結
簡單總結一下HashMap是使用了哪些方法來有效解決哈希沖突的:
1. 使用鍊位址法(使用散清單)來連結擁有相同hash值的資料;
2. 使用2次擾動函數(hash函數)來降低哈希沖突的機率,使得資料分布更平均;
3. 引入紅黑樹進一步降低周遊的時間複雜度,使得周遊更快;
能否使用任何類作為 Map 的 key?
可以使用任何類作為 Map 的 key,然而在使用之前,需要考慮以下幾點:
- 如果類重寫了 equals() 方法,也應該重寫 hashCode() 方法。
- 類的所有執行個體需要遵循與 equals() 和 hashCode() 相關的規則。
- 如果一個類沒有使用 equals(),不應該在 hashCode() 中使用它。
- 使用者自定義 Key 類最佳實踐是使之為不可變的,這樣 hashCode() 值可以被緩存起來,擁有更好的性能。不可變的類也可以確定 hashCode() 和 equals() 在未來不會改變,這樣就會解決與可變相關的問題了。
為什麼HashMap中String、Integer這樣的包裝類适合作為K?
答:String、Integer等包裝類的特性能夠保證Hash值的不可更改性和計算準确性,能夠有效的減少Hash碰撞的幾率
- 都是final類型,即不可變性,保證key的不可更改性,不會存在擷取hash值不同的情況
- 内部已重寫了
、equals()
等方法,遵守了HashMap内部的規範(不清楚可以去上面看看putValue的過程),不容易出現Hash值計算錯誤的情況;hashCode()
如果使用Object作為HashMap的Key,應該怎麼辦呢?
答:重寫
hashCode()
和
equals()
方法
- 重寫
是因為需要計算存儲資料的存儲位置,需要注意不要試圖從散列碼計算中排除掉一個對象的關鍵部分來提高性能,這樣雖然能更快但可能會導緻更多的Hash碰撞;hashCode()
- 重寫
方法,需要遵守自反性、對稱性、傳遞性、一緻性以及對于任何非null的引用值x,x.equals(null)必須傳回false的這幾個特性,目的是為了保證key在哈希表中的唯一性;equals()
HashMap為什麼不直接使用hashCode()處理後的哈希值直接作為table的下标?
答:
hashCode()
方法傳回的是int整數類型,其範圍為-(2 ^ 31)~(2 ^ 31 - 1),約有40億個映射空間,而HashMap的容量範圍是在16(初始化預設值)~2 ^ 30,HashMap通常情況下是取不到最大值的,并且裝置上也難以提供這麼多的存儲空間,進而導緻通過
hashCode()
計算出的哈希值可能不在數組大小範圍内,進而無法比對存儲位置;
那怎麼解決呢?
- HashMap自己實作了自己的
方法,通過兩次擾動使得它自己的哈希值高低位自行進行異或運算,降低哈希碰撞機率也使得資料分布更平均;hash()
- 在保證數組長度為2的幂次方的時候,使用
運算之後的值與運算(&)(數組長度 - 1)來擷取數組下标的方式進行存儲,這樣一來是比取餘操作更加有效率,二來也是因為隻有當數組長度為2的幂次方時,h&(length-1)才等價于h%length,三來解決了“哈希值與數組大小範圍不比對”的問題;hash()
HashMap 的長度為什麼是2的幂次方
為了能讓 HashMap 存取高效,盡量較少碰撞,也就是要盡量把資料配置設定均勻,每個連結清單/紅黑樹長度大緻相同。這個實作就是把資料存到哪個連結清單/紅黑樹中的算法。
這個算法應該如何設計呢?
我們首先可能會想到采用%取餘的操作來實作。但是,重點來了:“取餘(%)操作中如果除數是2的幂次則等價于與其除數減一的與(&)操作(也就是說 hash%length==hash&(length-1)的前提是 length 是2的 n 次方;)。” 并且 采用二進制位操作 &,相對于%能夠提高運算效率,這就解釋了 HashMap 的長度為什麼是2的幂次方。
那為什麼是兩次擾動呢?
答:這樣就是加大哈希值低位的随機性,使得分布更均勻,進而提高對應數組存儲下标位置的随機性&均勻性,最終減少Hash沖突,兩次就夠了,已經達到了高位低位同時參與運算的目的;
HashMap 與 HashTable 有什麼差別?
- 線程安全: HashMap 是非線程安全的,HashTable 是線程安全的;HashTable 内部的方法基本都經過
修飾。(如果你要保證線程安全的話就使用 ConcurrentHashMap 吧!);synchronized
- 效率: 因為線程安全的問題,HashMap 要比 HashTable 效率高一點。另外,HashTable 基本被淘汰,不要在代碼中使用它;
- 對Null key 和Null value的支援: HashMap 中,null 可以作為鍵,這樣的鍵隻有一個,可以有一個或多個鍵所對應的值為 null。但是在 HashTable 中 put 進的鍵值隻要有一個 null,直接抛NullPointerException。
- **初始容量大小和每次擴充容量大小的不同 **: ①建立時如果不指定容量初始值,Hashtable 預設的初始大小為11,之後每次擴充,容量變為原來的2n+1。HashMap 預設的初始化大小為16。之後每次擴充,容量變為原來的2倍。②建立時如果給定了容量初始值,那麼 Hashtable 會直接使用你給定的大小,而 HashMap 會将其擴充為2的幂次方大小。也就是說 HashMap 總是使用2的幂作為哈希表的大小,後面會介紹到為什麼是2的幂次方。
- 底層資料結構: JDK1.8 以後的 HashMap 在解決哈希沖突時有了較大的變化,當連結清單長度大于門檻值(預設為8)時,将連結清單轉化為紅黑樹,以減少搜尋時間。Hashtable 沒有這樣的機制。
- 推薦使用:在 Hashtable 的類注釋可以看到,Hashtable 是保留類不建議使用,推薦在單線程環境下使用 HashMap 替代,如果需要多線程使用則用 ConcurrentHashMap 替代。
如何決定使用 HashMap 還是 TreeMap?
對于在Map中插入、删除和定位元素這類操作,HashMap是最好的選擇。然而,假如你需要對一個有序的key集合進行周遊,TreeMap是更好的選擇。基于你的collection的大小,也許向HashMap中添加元素會更快,将map換為TreeMap進行有序key的周遊。
HashMap 和 ConcurrentHashMap 的差別
- ConcurrentHashMap對整個桶數組進行了分割分段(Segment),然後在每一個分段上都用lock鎖進行保護,相對于HashTable的synchronized鎖的粒度更精細了一些,并發性能更好,而HashMap沒有鎖機制,不是線程安全的。(JDK1.8之後ConcurrentHashMap啟用了一種全新的方式實作,利用CAS算法。)
- HashMap的鍵值對允許有null,但是ConCurrentHashMap都不允許。
ConcurrentHashMap 和 Hashtable 的差別?
ConcurrentHashMap 和 Hashtable 的差別主要展現在實作線程安全的方式上不同。
- 底層資料結構: JDK1.7的 ConcurrentHashMap 底層采用 分段的數組+連結清單 實作,JDK1.8 采用的資料結構跟HashMap1.8的結構一樣,數組+連結清單/紅黑二叉樹。Hashtable 和 JDK1.8 之前的 HashMap 的底層資料結構類似都是采用 數組+連結清單 的形式,數組是 HashMap 的主體,連結清單則是主要為了解決哈希沖突而存在的;
- 實作線程安全的方式(重要): ① 在JDK1.7的時候,ConcurrentHashMap(分段鎖) 對整個桶數組進行了分割分段(Segment),每一把鎖隻鎖容器其中一部分資料,多線程通路容器裡不同資料段的資料,就不會存在鎖競争,提高并發通路率。(預設配置設定16個Segment,比Hashtable效率提高16倍。) 到了 JDK1.8 的時候已經摒棄了Segment的概念,而是直接用 Node 數組+連結清單+紅黑樹的資料結構來實作,并發控制使用 synchronized 和 CAS 來操作。(JDK1.6以後 對 synchronized鎖做了很多優化) 整個看起來就像是優化過且線程安全的 HashMap,雖然在JDK1.8中還能看到 Segment 的資料結構,但是已經簡化了屬性,隻是為了相容舊版本;② Hashtable(同一把鎖) :使用 synchronized 來保證線程安全,效率非常低下。當一個線程通路同步方法時,其他線程也通路同步方法,可能會進入阻塞或輪詢狀态,如使用 put 添加元素,另一個線程不能使用 put 添加元素,也不能使用 get,競争會越來越激烈效率越低。
兩者的對比圖:
HashTable:
JDK1.7的ConcurrentHashMap:
JDK1.8的ConcurrentHashMap(TreeBin: 紅黑二叉樹節點 Node: 連結清單節點):
答:ConcurrentHashMap 結合了 HashMap 和 HashTable 二者的優勢。HashMap 沒有考慮同步,HashTable 考慮了同步的問題。但是 HashTable 在每次同步執行時都要鎖住整個結構。 ConcurrentHashMap 鎖的方式是稍微細粒度的。
ConcurrentHashMap 底層具體實作知道嗎?實作原理是什麼?
JDK1.7
首先将資料分為一段一段的存儲,然後給每一段資料配一把鎖,當一個線程占用鎖通路其中一個段資料時,其他段的資料也能被其他線程通路。
在JDK1.7中,ConcurrentHashMap采用Segment + HashEntry的方式進行實作,結構如下:
一個 ConcurrentHashMap 裡包含一個 Segment 數組。Segment 的結構和HashMap類似,是一種數組和連結清單結構,一個 Segment 包含一個 HashEntry 數組,每個 HashEntry 是一個連結清單結構的元素,每個 Segment 守護着一個HashEntry數組裡的元素,當對 HashEntry 數組的資料進行修改時,必須首先獲得對應的 Segment的鎖。
- 該類包含兩個靜态内部類 HashEntry 和 Segment ;前者用來封裝映射表的鍵值對,後者用來充當鎖的角色;
- Segment 是一種可重入的鎖 ReentrantLock,每個 Segment 守護一個HashEntry 數組裡得元素,當對 HashEntry 數組的資料進行修改時,必須首先獲得對應的 Segment 鎖。
JDK1.8
在JDK1.8中,放棄了Segment臃腫的設計,取而代之的是采用Node + CAS + Synchronized來保證并發安全進行實作,synchronized隻鎖定目前連結清單或紅黑二叉樹的首節點,這樣隻要hash不沖突,就不會産生并發,效率又提升N倍。
結構如下:
附加源碼,有需要的可以看看
插入元素過程(建議去看看源碼):
如果相應位置的Node還沒有初始化,則調用CAS插入相應的資料;
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value, null)))
break; // no lock when adding to empty bin
}
複制
如果相應位置的Node不為空,且目前該節點不處于移動狀态,則對該節點加synchronized鎖,如果該節點的hash不小于0,則周遊連結清單更新節點或插入新節點;
if (fh >= 0) {
binCount = 1;
for (Node<K,V> e = f;; ++binCount) {
K ek;
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
oldVal = e.val;
if (!onlyIfAbsent)
e.val = value;
break;
}
Node<K,V> pred = e;
if ((e = e.next) == null) {
pred.next = new Node<K,V>(hash, key, value, null);
break;
}
}
}
複制
- 如果該節點是TreeBin類型的節點,說明是紅黑樹結構,則通過putTreeVal方法往紅黑樹中插入節點;如果binCount不為0,說明put操作對資料産生了影響,如果目前連結清單的個數達到8個,則通過treeifyBin方法轉化為紅黑樹,如果oldVal不為空,說明是一次更新操作,沒有對元素個數産生影響,則直接傳回舊值;
- 如果插入的是一個新節點,則執行addCount()方法嘗試更新元素個數baseCount;
輔助工具類
Array 和 ArrayList 有何差別?
- Array 可以存儲基本資料類型和對象,ArrayList 隻能存儲對象。
- Array 是指定固定大小的,而 ArrayList 大小是自動擴充的。
- Array 内置方法沒有 ArrayList 多,比如 addAll、removeAll、iteration 等方法隻有 ArrayList 有。
對于基本類型資料,集合使用自動裝箱來減少編碼工作量。但是,當處理固定大小的基本資料類型的時候,這種方式相對比較慢。
如何實作 Array 和 List 之間的轉換?
- Array 轉 List: Arrays. asList(array) ;
- List 轉 Array:List 的 toArray() 方法。
comparable 和 comparator的差別?
- comparable接口實際上是出自java.lang包,它有一個 compareTo(Object obj)方法用來排序
- comparator接口實際上是出自 java.util 包,它有一個compare(Object obj1, Object obj2)方法用來排序
一般我們需要對一個集合使用自定義排序時,我們就要重寫compareTo方法或compare方法,當我們需要對某一個集合實作兩種排序方式,比如一個song對象中的歌名和歌手名分别采用一種排序方法的話,我們可以重寫compareTo方法和使用自制的Comparator方法或者以兩個Comparator來實作歌名排序和歌星名排序,第二種代表我們隻能使用兩個參數版的Collections.sort().
Collection 和 Collections 有什麼差別?
- java.util.Collection 是一個集合接口(集合類的一個頂級接口)。它提供了對集合對象進行基本操作的通用接口方法。Collection接口在Java 類庫中有很多具體的實作。Collection接口的意義是為各種具體的集合提供了最大化的統一操作方式,其直接繼承接口有List與Set。
- Collections則是集合類的一個工具類/幫助類,其中提供了一系列靜态方法,用于對集合中元素進行排序、搜尋以及線程安全等各種操作。
TreeMap 和 TreeSet 在排序時如何比較元素?Collections 工具類中的 sort()方法如何比較元素?
TreeSet 要求存放的對象所屬的類必須實作 Comparable 接口,該接口提供了比較元素的 compareTo()方法,當插入元素時會回調該方法比較元素的大小。TreeMap 要求存放的鍵值對映射的鍵必須實作 Comparable 接口進而根據鍵對元素進 行排 序。
Collections 工具類的 sort 方法有兩種重載的形式,
第一種要求傳入的待排序容器中存放的對象比較實作 Comparable 接口以實作元素的比較;
第二種不強制性的要求容器中的元素必須可比較,但是要求傳入第二個參數,參數是Comparator 接口的子類型(需要重寫 compare 方法實作元素的比較),相當于一個臨時定義的排序規則,其實就是通過接口注入比較元素大小的算法,也是對回調模式的應用(Java 中對函數式程式設計的支援)。
本文由來源 ThinkWon的部落格,由 system_mush 整理編輯,其版權均為 ThinkWon的部落格 所有,文章内容系作者個人觀點,不代表 Java架構師必看 對觀點贊同或支援。如需轉載,請注明文章來源。