什麼是集合java
集合框架:用於存儲數據的容器。node
集合框架是爲表示和操做集合而規定的一種統一的標準的體系結構。web
任何集合框架都包含三大塊內容:對外的接口、接口的實現和對集合運算的算法。算法
接口:表示集合的抽象數據類型。接口容許咱們操做集合時沒必要關注具體實現,從而達到「多編程
態」。在面向對象編程語言中,接口一般用來造成規範。設計模式
實現:集合接口的具體實現,是重用性很高的數據結構。數組
算法:在一個實現了某個集合框架中的接口的對象身上完成某種有用的計算的方法,例如查緩存
找、排序等。這些算法一般是多態的,由於相同的方法能夠在同一個接口被多個類實現時有安全
不一樣的表現。事實上,算法是可複用的函數。數據結構
它減小了程序設計的辛勞。
集合框架經過提供有用的數據結構和算法使你能集中注意力於你的程序的重要部分上,而不
是爲了讓程序能正常運轉而將注意力於低層設計上。
經過這些在無關 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 機制。
例如:假設存在兩個線程(線程 一、線程 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;
1
再看一下 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 defaultsnewCap = 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()、hashCode()等方法,遵照了 HashMap 內部的規範(不清楚能夠
去上面看看 putValue 的過程),不容易出現 Hash 值計算錯誤的狀況;
若是使用 Object 做爲 HashMap 的 Key,應該怎麼辦呢?
答:重寫 hashCode()和 equals()方法
重寫 hashCode()是由於須要計算存儲數據的存儲位置,須要注意不要試圖從散列碼計算中
排除掉一個對象的關鍵部分來提升性能,這樣雖然能更快但可能會致使更多的 Hash 碰撞;重寫 equals()方法,須要遵照自反性、對稱性、傳遞性、一致性以及對於任何非 null 的引
用值 x,x.equals(null)必須返回 false 的這幾個特性,目的是爲了保證 key 在哈希表中的惟
一性;
HashMap 爲何不直接使用 hashCode()處理後的哈希值直接做爲 table 的下標?
答:hashCode()方法返回的是 int 整數類型,其範圍爲-(2 ^ 31)~(2 ^ 31 - 1),約有 40
億個映射空間,而 HashMap 的容量範圍是在 16(初始化默認值)~2 ^ 30,HashMap
一般狀況下是取不到最大值的,而且設備上也難以提供這麼多的存儲空間,從而致使經過
hashCode()計算出的哈希值可能不在數組大小範圍內,進而沒法匹配存儲位置;
那怎麼解決呢?
HashMap 本身實現了本身的 hash()方法,經過兩次擾動使得它本身的哈希值高低位自行進
行異或運算,下降哈希碰撞機率也使得數據分佈更平均;
在保證數組長度爲 2 的冪次方的時候,使用 hash()運算以後的值與運算(&)(數組長度 -
1)來獲取數組下標的方式進行存儲,這樣一來是比取餘操做更加有效率,二來也是由於只
有當數組長度爲 2 的冪次方時,h&(length-1)纔等價於 h%length,三來解決了「哈希值
與數組大小範圍不匹配」的問題;
HashMap 的長度爲何是 2 的冪次方
爲了能讓 HashMap 存取高效,儘可能較少碰撞,也就是要儘可能把數據分配均勻,每一個鏈表
/紅黑樹長度大體相同。這個實現就是把數據存到哪一個鏈表/紅黑樹中的算法。
這個算法應該如何設計呢?咱們首先可能會想到採用%取餘的操做來實現。可是,重點來了:「取餘(%)操做中若是除
數 是 2 的 冪 次 則 等 價 於 與 其 除 數 減 一 的 與 (&) 操 做 ( 也 就 是 說
hash%length==hash&(length-1)的前提是 length 是 2 的 n 次方;)。」 而且 採用二
進制位操做 &,相對於%可以提升運算效率,這就解釋了 HashMap 的長度爲何是 2 的
冪次方。
那爲何是兩次擾動呢?
答:這樣就是加大哈希值低位的隨機性,使得分佈更均勻,從而提升對應數組存儲下標位置
的隨機性&均勻性,最終減小 Hash 衝突,兩次就夠了,已經達到了高位低位同時參與運算
的目的;
HashMap 與 HashTable 有什麼區別?
線程安全: HashMap 是非線程安全的,HashTable 是線程安全的;HashTable 內部的
方 法 基 本 都 經 過 synchronized 修 飾 。( 如 果 你 要 保 證 線 程 安 全 的 話 就 使 用
ConcurrentHashMap 吧!);
效率: 由於線程安全的問題,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
中對函數式編程的支持)。