java後端知識點梳理——java集合
集合概覽
Java中的集合,從上層介面上看分為了兩類,Map和Collection。Map是和Collection並列的集合上層介面,沒有繼承關係。
Java中的常見集合可以概括如下。
- Map介面和Collection介面是所有集合框架的父介面
- Collection介面的子介面包括:Set介面和List介面
- Map介面的實現類主要有:HashMap、TreeMap、HashtableLinkedHashMap、ConcurrentHashMap以及Properties等
- Set介面的實現類主要有:HashSet、TreeSet、LinkedHashSet等
- List介面的實現類主要有:ArrayList、LinkedList、Stack以及Vector等
HashMap和Hashtable的區別有哪些?
- HashMap沒有考慮同步,是執行緒不安全的;Hashtable使用了synchronized關鍵字,是執行緒安全的;
- HashMap允許null作為Key;Hashtable不允許null作為Key,Hashtable的value也不可以為null
HashMap是執行緒不安全的是吧?你可以舉一個例子嗎?
先別說快速失敗機制
- HashMap執行緒不安全主要是考慮到了多執行緒環境下進行擴容可能會出現HashMap死迴圈
- Hashtable執行緒安全是由於其內部實現在put和remove等方法上使用synchronized進行了同步,所以對單個方法的使用是執行緒安全的。但是對多個方法進行復合操作時,執行緒安全性無法保證。 比如一個執行緒在進行get然後put更新的操作,這就是兩個複合操作,在兩個操作之間,可能別的執行緒已經對這個key做了改動,所以,你接下來的put操作可能會不符合預期。
快速失敗(fast-fail)機制
快速失敗是Java集合的一種錯誤檢測機制,當多個執行緒對集合進行結構上的改變的操作時,有可能會產生fail-fast。
例如
假設存在兩個執行緒(執行緒1 、執行緒2) , 執行緒1通過Iterator 在遍歷集合 A 中的元素, 在某個時候執行緒2修改了集合A 的結構(是結構上面的修改,而 不是簡單的修改集合元素的內容), 那麼這個時候程式就會丟擲異常從而產生快速失敗機制。
原因
迭代器在遍歷時直接訪問集合中的內容, 並且在遍歷過程中使用—個 modCount 變數。集合在被遍歷期間如果內容發生變化, 就會改變modCount 的值。每當迭代器使用hashNext()/next()遍歷下一個元素之前, 都會檢測 modCount 變數是否為expectedmodCount 值,是的話就返回遍歷; 否則丟擲異常, 終止遍歷。
解決方法
- 在遍歷過程中, 所有涉及到改變modCount 值得地方全部加上synchronized。
- 使用執行緒安全的集合
hashmap
hashmap推薦看我的另一篇文章:
這裡簡單補充一下一致性Hash演算法
一致性Hash:
客戶端分片:雜湊+順時針(優化取餘)
節點伸縮:隻影響鄰近節點,但是還是有資料遷移
翻倍伸縮:保證最小遷移資料和負載均衡
一致性Hash可以很好的解決穩定問題,可以將所有的儲存節點排列在收尾相接的Hash環上,每個key在計算Hash後會順時針找到先遇到的一組儲存節點存放。而當有節點加入或退出時,僅影響該節點在Hash環上順時針相鄰的後續節點,將資料從該節點接收或者給予。但這有帶來均勻性的問題,即使可以將儲存節點等距排列,也會在儲存節點個數變化時帶來資料的不均勻。而這種可能成倍數的不均勻在實際工程中是不可接受的。
【未完待續】
ConcurrentHashMap和Hashtable的區別?
ConcurrentHashMap結合了HashMap和Hashtable二者的優勢。HashMap沒有考慮同步,Hashtable考慮了同步的問題。但是Hashtable在每次同步執行時都要鎖住整個結構。
ConcurrentHashMap鎖的方式是稍微細粒度的,ConcurrentHashMap將hash表分為16個桶(預設值),諸如get,put,remove等常用操作只鎖上當前需要用到的桶。
ConcurrentHashMap的具體實現方式(分段鎖)
該類包含兩個靜態內部類MapEntry和Segment,前者用來封裝對映表的鍵值對,後者用來充當鎖的角色。
segment
Segment是一種可重入的鎖ReentrantLock,每個Segment守護一個HashEntry數組裡得元素,當對HashEntry陣列的資料進行修改時,必須首先獲得對應的Segment鎖。
在JDK1.7及其之前ConcurrentHashMap實現執行緒安全的方法相對比較簡單:
- 其內部將資料分為數個“段(Segment)”,其數量和併發級別有關係,具體是“大於等於併發級別的最小的2的冪次”。
- 每個segment使用單獨的ReentrantLock(分段鎖)。
- 如果操作涉及不同segment,則可以併發執行,如果是同一個segment則會進行鎖的競爭和等待。
- 此設計的效率是高於synchronized的。
不過JDK8之後,ConcurrentHashMap捨棄了ReentrantLock,而重新使用了synchronized。其原因大致有一下幾點:
- 加入多個分段鎖浪費記憶體空間。
- 生產環境中, map 在放入時競爭同一個鎖的概率非常小,分段鎖反而會造成更新等操作的長時間等待。
- 為了提高 GC 的效率
新的ConcurrentHashMap中使用synchronized關鍵字+CAS操作保證了執行緒安全。
TreeMap有哪些特性?
TreeMap底層使用紅黑樹實現,TreeMap中儲存的鍵值對按照鍵來排序。
- 如果Key存入的是字串等型別,那麼會按照字典預設順序排序
- 如果傳入的是自定義引用型別,比如說User,那麼該物件必須實現Comparable介面,並且覆蓋其compareTo方法;或者在建立TreeMap的時候,我們必須指定使用的比較器。
如下所示:
// 方式一:定義該類的時候,就指定比較規則
class User implements Comparable{
@Override
public int compareTo(Object o) {
// 在這裡邊定義其比較規則
return 0;
}
}
public static void main(String[] args) {
// 方式二:建立TreeMap的時候,可以指定比較規則
new TreeMap<User, Integer>(new Comparator<User>() {
@Override
public int compare(User o1, User o2) {
// 在這裡邊定義其比較規則
return 0;
}
});
}
引申:那麼Comparable介面和Comparator介面有哪些區別呢?
- Comparable實現比較簡單,但是當需要重新定義比較規則的時候,必須修改原始碼,即修改User類裡邊的compareTo方法
- Comparator介面不需要修改原始碼,只需要在建立TreeMap的時候重新傳入一個具有指定規則的比較器即可。
補充:comparable 和comparator的區別?
comparable接口出自java.lang包,它有一個compareTo(Object obj)方法用來排序
comparator接口出自java.util包,它有一個compare(Object obj1 Object obj2) 方法用來排序
ArrayList和LinkedList有哪些區別?
- ArrayList底層使用了動態陣列實現,實質上是一個動態陣列
- LinkedList底層使用了雙向連結串列實現,可當作堆疊、佇列、雙端佇列使用
- ArrayList在隨機存取方面效率高於LinkedList
- LinkedList在節點的增刪方面效率高於ArrayList
- ArrayList必須預留一定的空間,當空間不足的時候,會進行擴容操作
- LinkedList的開銷是必須儲存節點的資訊以及節點的指標資訊
其實還有一個集合Vector,它是執行緒安全的ArrayList,但是已經被廢棄,不推薦使用了。多執行緒環境下,我們可以使用CopyOnWriteArrayList替代ArrayList來保證執行緒安全。
HashSet和TreeSet有哪些區別?
- HashSet底層使用了Hash表實現。
- 保證元素唯一性的原理:判斷元素的hashCode值是否相同。如果相同,還會繼續判斷元素的equals方法,是否為true
- TreeSet底層使用了紅黑樹來實現。
- 保證元素唯一性是通過Comparable或者Comparator介面實現
HashSet和HashMap
其實,HashSet的底層實現還是HashMap,只不過其只使用了其中的Key,具體如下所示:
- HashSet的add方法底層使用HashMap的put方法將key = e,value=PRESENT構建成key-value鍵值對,當此e存在於HashMap的key中,則value將會覆蓋原有value,但是key保持不變,所以如果將一個已經存在的e元素新增中HashSet中,新新增的元素是不會儲存到HashMap中,所以這就滿足了HashSet中元素不會重複的特性。
- HashSet的contains方法使用HashMap得containsKey方法實現
LinkedHashMap和LinkedHashSet有了解嗎?
LinkedHashMap內部的Entry繼承於HashMap.Node,這兩個類都實現了Map.Entry<K,V>
LinkedHashMap的Entry不光有value,next,還有before和after屬性,這樣通過一個雙向連結串列,保證了各個元素的插入順序
通過構造方法public LinkedHashMap(int initialCapacity,float loadFactor,boolean accessOrder), accessOrder傳入true可以實現LRU快取演算法(訪問順序)
LinkedHashSet 底層使用LinkedHashMap實現,兩者的關係類似與HashMap和HashSet的關係,大家可以自行類比。
什麼是LRU演算法?LinkedHashMap如何實現LRU演算法?
LRU(Least recently used,最近最少使用)演算法根據資料的歷史訪問記錄來進行淘汰資料,其核心思想是“如果資料最近被訪問過,那麼將來被訪問的機率也更高”。思路如下
- 新資料插入到連結串列頭部;
- 每當快取命中(即快取資料被訪問),則將資料移到連結串列頭部;
- 當連結串列滿的時候,將連結串列尾部的資料丟棄。
關於【命中率】
當存在熱點資料時,LRU的效率很好,但偶發性的、週期性的批量操作會導致LRU命中率急劇下降,快取汙染情況比較嚴重。
import java.util.LinkedHashMap;
import java.util.Map;
public class LRUTest {
private static int size = 5;
public static void main(String[] args) {
Map<String, String> map = new LinkedHashMap<String, String>(size, 0.75f, true) {
@Override
protected boolean removeEldestEntry(Map.Entry<String, String> eldest) {
return size() > size;
}
};
map.put("1", "1");
map.put("2", "2");
map.put("3", "3");
map.put("4", "4");
map.put("5", "5");
System.out.println(map.toString());
map.put("6", "6");
System.out.println(map.toString());
map.get("3");
System.out.println(map.toString());
map.put("7", "7");
System.out.println(map.toString());
map.get("5");
System.out.println(map.toString());
}
}
List和Set的區別?
- List是有序的並且元素是可以重複的
- Set是無序(LinkedHashSet除外)的,並且元素是不可以重複的
說明: 此處的有序和無序是指放入順序和取出順序是否保持一致
Iterator和ListIterator的區別是什麼?
- Iterator可以遍歷list和set集合;ListIterator只能用來遍歷list集合
- Iterator前者只能前向遍歷集合;ListIterator可以前向和後向遍歷集合
- ListIterator其實就是實現了前者,並且增加了一些新的功能。
Iterato參考程式碼:
ArrayList<String> list = new ArrayList<>();
list.add("zhangsan");
list.add("lisi");
list.add("yangwenqiang");
// 建立迭代器實現遍歷集合
Iterator<String> iterator = list.iterator();
while(iterator.hasNext()){
System.out.println(iterator.next());
}
Collection和Collections有什麼關係?
Collection和Collections的關係: Collection是一個頂層集合介面,其子介面包括List和Set;而Collections是一個集合工具類,可以操作集合,比如說排序,二分查詢,拷貝集合,尋找最大最小值等。 總而言之:帶s的大都是工具類。
說在最後
附上集合介面和實現類的關係
如果感興趣,可以配合我的java集合面試題來看。