Collection 單列集合
List 集合
List 集合的三個子類:
ArrayList:底層是數組,查詢快(地址連續)、增刪慢、線程非安全。
LinkedList:底層是鏈表,查詢慢、增刪快、無索引、線程非安全。
Vector:底層是數組,線程安全。
一、ArrayList 原理
1.1 構造方法
ArrayList 底層是一個數組結構,當指定初始容量爲0時返回的是 EMPTY_ELEMENTDATA,不指定容量時返回 DEFAULTCAPACITY_EMPTY_ELEMENTDATA。
1.2 add 方法
1. 確認list容量,嘗試容量加1看有無必要,因爲當添加一個元素後,可能對超過集合初始容量,將會引發擴容。如果沒有超過初始容量則保存最小容量不變,無需對容量+1。
2. 添加元素
第一個方法得到數組的最小容量,避免資源浪費,第二個方法判斷最小容量是否大於數組長度,大於則調用grow()進行擴容。
grow()擴容的實現
newCapacity 這裏對數組容量擴容1.5倍,擴容完畢後調用 copyOf() ,完成對數組元素的拷貝。
總的來說 add(E e) 的實現: 首先檢查數組容量是否足夠,夠直接添加元素,不夠進行擴容。
1.3 get 方法
檢查角標是否越界,然後添加元素。
1.4 set 方法
檢查是否越界,替換元素。
1.5 remove() 方法
numMoved 是需要移動的元素個數,從 index+1位置開始拷貝 numMoved 個元素 到數組的 index 位置,就等於將被刪除元素後面的所有元素往前移動一個位置。因此List集合刪除效率低。
二、Vector 和 ArrayList 區別(面試)
Vector 是同步的(線程安全),其內部方法都使用 synchronized 修飾,同樣Vector因此效率比較低下。ArrayList 線程非安全。
ArrayList 底層擴容是在原有基礎上擴容1.5 倍,Vector是擴容 2 倍。
三、LinkedList 原理
LinkedList 集合特點:
1. 底層是雙向鏈表,查詢慢,增刪快。 2. 包含大量操作首尾的方法。
3.1 add 方法
瞭解過鏈表底層實現的應該對 LinkedList 的方法並不陌生,無論是添加還是刪除方法,都拆分爲 對頭部和尾部元素的具體操作。
這裏 add 方法採用尾插法,同樣也有 addFirst addLast 方法。
3.2 remove 方法
unlink(x) 完成對元素的刪除,刪除元素調用 equals 方法其實就是判斷元素是否存在鏈表中,unlink 方法中實現了雙向鏈表中刪除元素的操作。
將被刪除節點斷開,通過①②連接鏈表。
3.3 get 方法
判斷下標是否合法,然後調用node方法獲取鏈表元素。
3.4 set 方法
與 get 方法類似,根據下標判斷從頭遍歷還是從尾遍歷。
Set 集合
Set 集合的使用頻率相比 List 集合較低,通常結合 set 集合 元素不可重複的特點進行一些操作,比如尋找只出現一次的數字、或者在實際項目中需要存儲不可重複元素可以考慮使用 Set 集合。
一、Set集合常用子類
-
HashSet集合
-
A:底層數據結構是HashMap
-
B:允許null,非同步
-
-
TreeSet集合
-
A:底層數據結構是紅黑樹(是一個自平衡的二叉樹)
-
B:保證元素的排序方式,不允許null,非同步
-
-
LinkedHashSet集合
-
A:底層數據結構由HashMap和雙向鏈表組成。
-
B:允許null
-
二、HashSet集合
java.util.HashSet 集合 implements Set接口
HashSet是根據對象的哈希值來確定元素在集合中的存儲位置,因此具有良好的存取和查找性能。保證元素唯一性的方式依賴於:hashCode與equals方法。
2.1 HashSet特點:
1.不允許存儲重複元素,允許元素爲null
2.沒有索引,沒有帶索引的方法,也沒有for循環遍歷
3.是一個無序的集合,存儲元素和取出元素的順序有可能不一致
4.底層是HashMap(查詢速度非常快)
Set<Integer> set = new HashSet<>(); //多態
//使用add方法往集合中添加元素
set.add(1);
set.add(3);
set.add(2);
set.add(1);
//使用迭代器遍歷set集合
Iterator<Integer> it = set.iterator();
while(it.hasNext()){
Integer n = it.next();
System.out.println(n); //1,2,3 不允許存儲重複元素,無序
}
//增強for遍歷set集合
for(Integer n:set){
System.out.print(n);
}
2.2 HashSet集合存儲結構(哈希表)
在JDK1.8之前,哈希表底層採用數組+鏈表實現,即使用鏈表處理哈希衝突,同一hash值的鏈表都存儲在一個鏈表裏。但是當hash值相等的元素較多時,通過key值在鏈表中依次查找的效率較低。JDK1.8中,哈希表存儲採用數組+鏈表+紅黑樹實現,當鏈表長度超過(8)時,將鏈表轉換爲紅黑樹,這樣大大減少了查找時間。
要保證HashSet集合元素的唯一,就必須複寫hashCode和equals方法建立屬於當前對象的比較方式。(面試常考hashCode 和 equals 的聯繫)
① 重寫 hashCode 必須同時重寫equals。
② 相等(相同)的對象必須具有相等的哈希碼(或者散列碼)。
③ 如果兩個對象的hashCode相同,它們並不一定相同。
Map 雙列集合
java.util.Map<k,v>集合 存儲的元素是鍵值對類型,key - value是一一對應的映射關係。
Map集合特點:
key 和 value 數據類型是任意的,一般我們習慣使用key爲String類型,value爲Object類型。
key 不允許重複,value 可以重複,key只允許存在一個null的情況,value可以存在多個null值。
一、Collection 與 Map的區別(面試)
1. Map 集合存儲元素是鍵值對的方式,鍵唯一,值可以重複。
2. Collection 集合存儲元素是單獨出現的,Collection 的子接口Set值是唯一的,List是可重複的。
二、哈希表
List 集合底層是鏈表或者數組,存儲的順序和取出的順序是一致的,但同樣會帶來一個缺點,想要獲取某個元素,就要訪問所有元素,直到找到爲止。
在哈希表中,我們不在意元素的順序,能夠快速查找到元素。哈希表中每個對象按照哈希值保存在對應的位置上。哈希表是由數組+鏈表實現的,每個列表稱爲桶,
JDK1.8 之前 哈希值相同的對象以鏈表形式存儲在一個桶中,這種情況稱爲哈希衝突,此時需要用該對象與桶上的對象進行比較,看看是否已經存在桶中,如果存在就不添加。(這個過程使用hashCode和equals進行判斷,這也是之前我們說過爲什麼要同時重寫hashCode和equals的原因),接下來了解了HashMap的put方法底層原理,應該會對爲什麼同時重寫更加清晰了。
JDK1.8 之後 當鏈表長度超過8,會從鏈表轉換爲紅黑樹結構。當哈希表存儲元素太多,需要對其進行擴展,創建一個桶更多的哈希表,那麼什麼時候需要進行擴容?裝填因子(load factor)決定了何時進行擴展,裝載因子默認爲0.75,表中如果超過75%的位置已經填入元素,將進行雙倍的擴展。
三、HashMap 原理
3.1 HashMap 類前註釋解讀
主要講了 HashMap 的一些特點:key value 允許爲null,不保證有序,不同步(線程非安全),想實現同步調用Collections.synchronizedMap,裝載因子爲0.75。使用迭代器進行遍歷。當知道要存儲的元素個數時,儘可能設置初始容量。
3.2 HashMap 屬性
當鏈表長度大於8,數組元素超過64 由鏈表結構轉化爲紅黑樹。
JDK1.7 時,在實例化之後,底層創建了長度是16的Entry數組。
JDK1.8 時,底層爲Node數組,調用put方法後創建長度爲16的數組。雖然長度爲16,但是切記,負載因子0.75,因此數組只存放12個元素。
3.3 put 方法
put 方法是HashMap的核心,也是面試常考點。
調用 hash(key) 方法,以key計算哈希值,hash 方法中得到key的hashCode 與 key的hashCode 高16位做異或運算,得到的值可以減少碰撞衝突的可能性。
1、hash(key),取key的hashcode進行高位運算,返回hash值
2、如果hash數組爲空,直接resize()
3、對hash進行取模運算計算,得到key-value在數組中的存儲位置i
(1)如果table[i] == null,直接插入Node<key,value>
(2)如果table[i] != null,判斷是否爲紅黑樹p instanceof TreeNode。
(3)如果是紅黑樹,則判斷TreeNode是否已存在,如果存在則直接返回oldnode並更新;不存在則直接插入紅黑樹,++size,超出threshold容量就擴容
(4)如果是鏈表,則判斷Node是否已存在,如果存在則直接返回oldnode並更新;不存在則直接插入鏈表尾部,判斷鏈表長度,如果大於8則轉爲紅黑樹存儲,++size,超出threshold容量就擴容
3.4 get 方法
我們知道HashMap 是通過key找value,獲取key的哈希值,調用getNode方法獲取對應value。
getNode 的實現比較簡單,先判斷計算出來的hashCode是否存在哈希表中,然後判斷是否在桶的首位,不在則在鏈表或紅黑樹中進行遍歷。
3.5 remove 方法
類似於put方法,這裏就不貼源碼,可以在util包下查看源碼。
計算key 的hash值,然後去找對應節點,找到後分三種情況進行刪除:1. 鏈表 2. 紅黑樹 3. 桶的首位
四、HashMap 與 Hashtable 對比
1、繼承的父類不同
Hashable繼承自Dictionary類,而HashMap繼承自AbstractMap類。但二者都實現了Map接口。
2、線程安全性不同
HashTable是線程安全的,HashTable方法都加入了Synchronized,HashMap是非安全的。
3、是否提供contains方法
HashMap把contains方法去掉了,改成containsValue和containsKey,因爲contains方法容易讓人引起誤解。
Hashtable則保留了contains,containsValue和containsKey三個方法,其中contains和containsValue功能相同。
4、key和value是否允許null值
其中key和value都是對象,並且不能包含重複key,但可以包含重複的value。
HashMap允許空值,鍵key最多隻能1個null值,value允許多個null,HashMap中不能由get()方法來判斷HashMap中是否存在某個鍵, 而應該用containsKey()方法來判斷。
Hashtable中,key和value都不允許出現null值。
5、兩個集合遍歷方式的內部實現上不同
Hashtable、HashMap都使用了 Iterator。而由於歷史原因,Hashtable還使用了Enumeration的方式
6、hash值不同
哈希值的使用不同,HashTable直接使用對象的hashCode。而HashMap重新計算hash值。
7、內部實現使用的數組初始化和擴容方式不同
HashTable在不指定容量的情況下的默認容量爲11,而HashMap爲16,Hashtable不要求底層數組的容量一定要爲2的整數次冪,而HashMap則要求一定爲2的整數次冪。
圖文並茂,對比很細,加粗部分可作爲面試主答點。
五、LinkedHashMap 原理
5.1 LinkedHashMap 類前註釋解讀
底層是哈希表+鏈表,允許爲null,線程不同步,插入元素有序
5.2 總結
這裏沒有對LinkedHashMap具體方法進行解析,因爲它繼承自HashMap,在很多操作上使用的是HashMap的方法。而HashMap的方法我們上面已經進行了分析。主要是個人對LinkedHashMap無感,平時也基本沒使用,大家有興趣可以看一下源碼。
六、TreeMap 原理
6.1 TreeMap 類前註釋解讀
底層是紅黑樹,實現NavigableMap 接口,可以根據key自然排序,也可以在構造方法上傳遞Camparator實現Map的排序。不同步
TreeMap 實現NavigableMap 接口,而NavigableMap接口繼承着SortedMap接口,致使我們的TreeMap是有序的。
key不能爲null。
6.2 構造方法
//comparator 維護的變量爲null,使用自然排序
public TreeMap() {
comparator = null;
}
//設置一個維護變量傳入
public TreeMap(Comparator<? super K> comparator) {
this.comparator = comparator;
}
//調用putAll將Map轉爲TreeMap
public TreeMap(Map<? extends K, ? extends V> m) {
comparator = null;
putAll(m);
}
//使用buildFromSorted轉TreeMap
public TreeMap(SortedMap<K, ? extends V> m) {
comparator = m.comparator();
try {
buildFromSorted(m.size(), m.entrySet().iterator(), null, null);
} catch (java.io.IOException cannotHappen) {
} catch (ClassNotFoundException cannotHappen) {
}
}
6.3 put 方法
6.4 get 方法
get 方法由getEntry進行具體實習
getEntry
當 comparator 不爲null時,getEntryUsingComparator(Object key) 調用Comparator 自己實現的方法獲取相應的值。
6.5 總結
TreeMap底層是紅黑樹,能夠實現該Map集合有序
如果在構造方法中傳遞了Comparator對象,那麼就會以Comparator對象的方法進行比較。否則,則使用Comparable的compareTo(T o)
方法來比較。(自然排序)
-
值得說明的是:如果使用的是
compareTo(T o)
方法來比較,key一定是不能爲null,並且得實現了Comparable接口的。 -
即使是傳入了Comparator對象,不用
compareTo(T o)
方法來比較,key也是不能爲null的。
七、ConcurrentHashMap
重頭戲來了~~~~
7.1 JDK1.7和JDK1.8 不同
1. JDK1.7 底層實現是:segments+HashEntry數組
ConcurrentHashMap使用分段鎖技術,Segment繼承了ReentrantLock,每個片段都有了一個鎖,叫做“鎖分段”
我們知道Map中Hashtable是線程安全的,爲什麼要用ConCurrentHashMap?
Hashtable 是在每個方法上都加上了Synchronized 完成同步,效率極其低下,而ConcurrentHashMap通過在部分加鎖和利用CAS算法來實現同步。
2. JDK 1.8底層實現是:哈希表+紅黑樹
檢索操作不用加鎖,get方法是非阻塞的,key和value都不允許爲空。
取消了1.7的 segments 分段鎖機制,採用CAS + volatile 樂觀鎖機制保證線程同步。
7.2 CAS算法和volatile簡單介紹
1. CAS(Compare and swap,比較與交換)
CAS 是一種基於樂觀鎖的操作。在java中鎖分爲樂觀鎖和悲觀鎖。悲觀鎖是將資源鎖住,等一個之前獲得鎖的線程釋放鎖之後,下一個線程纔可以訪問。而樂觀鎖採取了一種寬泛的態度,通過某種方式不加鎖來處理資源,比如通過給記錄加version來獲取數據,性能較悲觀鎖有很大的提高。
CAS有3個操作數
-
內存值V
-
舊的預期值A
-
要修改的新值B
當且僅當預期值A和內存值V相同時,將內存值V修改爲B。CAS是通過無限循環來獲取數據的,若果在第一輪循環中,a線程獲取地址裏面的值被b線程修改了,那麼a線程需要自旋,到下次循環纔有可能機會執行。
2. volatile 關鍵字
volatile僅僅用來保證該變量對所有線程的可見性,但不保證原子性
可見性:在多線程環境下,當被volatile修飾的變量修改時,所有的線程都可以知道該變量被修改了
不保證原子性:修改變量(賦值)實質上是分爲好幾步操作,在這幾步操作中是不安全的。
7.2 put 方法
put操作採用CAS+synchronized 實現併發插入或更新操作。
- 如果沒有初始化就先調用initTable()方法來進行初始化過程
- 如果沒有hash衝突就直接CAS插入
- 如果還在進行擴容操作就先進行擴容
- 如果存在hash衝突,就加 synchronized 鎖來保證線程安全,這裏有兩種情況,一種是鏈表形式就直接遍歷到尾端插入,一種是紅黑樹就按照紅黑樹結構插入,
- 最後一個如果Hash衝突時會形成Node鏈表,在鏈表長度超過8,Node數組超過64時會將鏈表結構轉換爲紅黑樹的結構,break再一次進入循環
- 如果添加成功就調用addCount()方法統計size,並且檢查是否需要擴容
initTable 方法的實現
當要初始化時會通過CAS操作將sizeCtl置爲-1,而sizeCtl由volatile修飾,保證修改對後面線程可見。
這之後如果再有線程執行到此方法時檢測到sizeCtl爲負數,說明已經有線程在給擴容了,這個線程就會調用Thread.yield()
讓出一次CPU執行時間。
7.3 get 方法
我們之前說到 get 操作不用加鎖,是非阻塞的,因爲 Node 節點被重寫了,設置了volatile關鍵字修飾,致使它每次獲取的都是最新設置的值。
八、CopyOnWriteArrayList 原理
CopyOnWriteArrayList是線程安全容器(相對於ArrayList),底層通過複製數組的方式來實現。
CopyOnWriteArrayList在遍歷的使用不會拋出ConcurrentModificationException異常,並且遍歷的時候就不用額外加鎖。
8.1 CopyOnWriteArrayList 基本結構
CopyOnWriteArrayList底層是數組,加鎖交由ReentrantLock來完成
//可重入鎖對象
final transient ReentrantLock lock = new ReentrantLock();
//CopyOnWriteArrayList底層由數組實現,volatile修飾
private transient volatile Object[] array;
//得到數組
final Object[] getArray() {
return array;
}
//設置數組
final void setArray(Object[] a) {
array = a;
}
//初始化數組
public CopyOnWriteArrayList() {
setArray(new Object[0]);
}
8.2 add 方法
public boolean add(E e) {
//加鎖
final ReentrantLock lock = this.lock;
lock.lock();
try {
//得到原數組長度和元素
Object[] elements = getArray();
int len = elements.length;
//複製一個新數組
Object[] newElements = Arrays.copyOf(elements, len + 1);
//將元素添加到新數組
newElements[len] = e;
//將volatile Object[] array 的指向替換成新數組
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}
在添加的時候上鎖,並複製一個新數組,添加操作在新數組上完成,將array指向到新數組中,最後解鎖。
8.3 set 方法
public E set(int index, E element) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
//得到原數組舊值
Object[] elements = getArray();
E oldValue = get(elements, index);
//判斷新值和舊值是否相等
if (oldValue != element) {
int len = elements.length;
//複製新數組,在新數組中完成修改
Object[] newElements = Arrays.copyOf(elements, len);
newElements[index] = element;
//array引用指向新數組
setArray(newElements);
} else {
// Not quite a no-op; ensures volatile write semantics
setArray(elements);
}
return oldValue;
} finally {
lock.unlock();
}
}
-
在修改時,複製出一個新數組,修改的操作在新數組中完成,最後將新數組交由array變量指向。
-
寫加鎖,讀不加鎖,讀取數據是在原數組,因此保證了多線程下的數據一致性。
-
類似於讀寫分離的思想,讀和寫分別在不同的容器完成,寫時進行讀操作並不會讀到髒數據。
8.4 CopyOnWriteArrayList缺點
佔用內存:如果CopyOnWriteArrayList經常要增刪改裏面的數據,經常要執行add()、set()、remove()
進行新數組創建,那是比較耗費內存的。
數據一致性:CopyOnWrite容器只能保證數據的最終一致性,不能保證數據的實時一致性。
面試題總結
上面標註(面試)的這裏就不再闡述。。。
List 和 Map的區別
存儲結構不同:List 是存儲單列的集合,Map是存儲key-value鍵值對的集合
元素是否可重複:List 允許重複元素,Map不允許key重複
是否有序:List集合是有序的(存儲有序),Map集合是無序的。
Collection和Collections的區別
Collection是集合的上級接口,繼承它的有Set和List接口
Collections是集合的工具類,提供了一系列的靜態方法對集合的搜索、查找、同步等操作
ArrayList,Vector,LinkedList的存儲性能和特徵?
ArrayList和Vector都是使用數組方式存儲元素,可以直接按照索引查找元素、訪問快、增刪慢(相對的,極端情況不考慮)。
LinkedList使用雙向鏈表存儲數據、查找慢、增刪快(相對的,極端情況不考慮)。
Vector使用了synchronized方法,線程安全,性能上較ArrayList差。LinkedList也是線程不安全的,LinkedList提供了一些方法,使得它可以被當作堆棧或者隊列來使用。
Java中HashMap的key值要是爲類對象則該類需要滿足什麼條件?
需要同時重寫該類的hashCode()方法和equals方法。
原因:
插入元素時先計算hashCode值,相等則調用 equals() 方法,兩個key相同,替換元素值,兩個key不相同,說明 hashCode 值是碰巧相同,則將新增元素放在桶裏。
我們一般認爲,只要兩個對象的成員變量的值是相等的,那麼我們就認爲這兩個對象是相等的,因此要重寫equals()f方法,重寫了equals(),就必須重寫hashCode()方法,因爲equals()認定了這兩個對象相同,而同一個對象調用hashCode()方法時,是應該返回相同的值的!