學(xué)習(xí)集合類源碼對(duì)我們實(shí)際工作的幫助和應(yīng)用!
Java的集合類包括Map和Collection兩大類。Collection包括List、Set和Queue三個(gè)小類。
「如下圖:」
這邊文章通過源碼解讀的方式帶大家了解一下:集合類使用過程中常見的問題以及學(xué)習(xí)一些優(yōu)秀的設(shè)計(jì)思想。
「集合批量操作性能」
集合的單個(gè)操作,一般都沒有性能問題,性能問題主要出現(xiàn)的批量操作上。
如批量新增操作:
在 List 和 Map 大量數(shù)據(jù)新增的時(shí)候,使用 for 循環(huán) + add/put 方法新增,這樣子會(huì)有很大的擴(kuò)容成本,我們應(yīng)該盡量使用 addAll 和 putAll 方法進(jìn)行新增,如下演示了兩種方案的性能對(duì)比:
單個(gè) for 循環(huán)新增 300 w 個(gè),耗時(shí)1518。
批量新增 300 w 個(gè),耗時(shí)8。
可以看到,批量新增方法性能是單個(gè)新增方法性能的 189 倍,主要原因在于批量新增,只會(huì)擴(kuò)容一次,大大縮短了運(yùn)行時(shí)間,而單個(gè)新增,每次到達(dá)擴(kuò)容閥值時(shí),都會(huì)進(jìn)行擴(kuò)容,在整個(gè)過程中就會(huì)不斷的擴(kuò)容,浪費(fèi)了很多時(shí)間。
我們來看下批量新增的源碼:
我們可以看到,整個(gè)批量新增的過程中,只擴(kuò)容了一次。
「集合線程安全性」
集合的非線程安全指的是:集合類作為共享變量,被多線程讀寫的時(shí)候是不安全的,如果要實(shí)現(xiàn)線程安全的集合,在類注釋中,JDK 統(tǒng)一推薦我們使用 Collections.synchronized* 類。
Collections 幫我們實(shí)現(xiàn)了 List、Set、Map 對(duì)應(yīng)的線程安全的方法, 如下圖:
從源碼中我們可以看到 Collections 是通過 synchronized 關(guān)鍵字給 List 操作數(shù)組的方法加上鎖,來實(shí)現(xiàn)線程安全的。
集合類方法常見的問題?
List?
「Arrays.asList()方法」
我們把數(shù)組轉(zhuǎn)化成集合時(shí),常使用 Arrays.asList(array),這個(gè)方法有兩個(gè)問題,代碼演示如下:
問題一:修改數(shù)組的值,會(huì)直接影響原list。
public void testArrayToList(){
Integer[] array = new Integer[]{1,2,3,4,5,6};
List<Integer> list = Arrays.asList(array);
// 問題1:修改數(shù)組的值,會(huì)直接影響原 list
log.info("數(shù)組被修改之前,集合第一個(gè)元素為:{}",list.get(0));
array[0] = 10;
log.info("數(shù)組被修改之前,集合第一個(gè)元素為:{}",list.get(0));
}
問題二:不能對(duì)新 List 進(jìn)行 add、remove 等操作,否則運(yùn)行時(shí)會(huì)報(bào) UnsupportedOperationException 錯(cuò)誤。
public void testArrayToList(){
Integer[] array = new Integer[]{1,2,3,4,5,6};
List<Integer> list = Arrays.asList(array);
// 問題2:使用 add、remove 等操作 list 的方法時(shí),
// 會(huì)報(bào) UnsupportedOperationException 異常
list.add(7);
}
原因分析:
從上圖中,我們可以發(fā)現(xiàn),Arrays.asList? 方法返回的 List 并不是 java.util.ArrayList,而是自己內(nèi)部的一個(gè)靜態(tài)類,該靜態(tài)類直接持有數(shù)組的引用,并且沒有實(shí)現(xiàn) add、remove 等方法,這些就是問題 1 和 2 的原因。
「list.toArray方法」
public void testListToArray(){
List<Integer> list = new ArrayList<Integer>(){{
add(1);
add(2);
add(3);
add(4);
}};
// 下面這行代碼是無法轉(zhuǎn)化成數(shù)組的,無參 toArray 返回的是 Object[],
// 無法向下轉(zhuǎn)化成 List<Integer>,編譯都無法通過
// List<Integer> list2 = list.toArray();
// 有參 toArray 方法,數(shù)組大小不夠時(shí),得到數(shù)組為 null 情況
Integer[] array0 = new Integer[2];
list.toArray(array0);
log.info("toArray 數(shù)組大小不夠,array0 數(shù)組[0] 值是{},數(shù)組[1] 值是{},",array0[0],array0[1]);
// 數(shù)組初始化大小正好,正好轉(zhuǎn)化成數(shù)組
Integer[] array1 = new Integer[list.size()];
list.toArray(array1);
log.info("toArray 數(shù)組大小正好,array1 數(shù)組[3] 值是{}",array1[3]);
// 數(shù)組初始化大小大于實(shí)際所需大小,也可以轉(zhuǎn)化成數(shù)組
Integer[] array2 = new Integer[list.size()+2];
list.toArray(array2);
log.info("toArray 數(shù)組大小多了,array2 數(shù)組[3] 值是{},數(shù)組[4] 值是{}",array2[3],array2[4]);
}
toArray 數(shù)組大小不夠,array0 數(shù)組[0] 值是null,數(shù)組[1] 值是null,
toArray 數(shù)組大小正好,array1 數(shù)組[3] 值是4
toArray 數(shù)組大小多了,array2 數(shù)組[3] 值是4,數(shù)組[4] 值是null
原因分析:
toArray 的無參方法,無法強(qiáng)轉(zhuǎn)成具體類型,這個(gè)編譯的時(shí)候,就會(huì)有提醒,我們一般都會(huì)去使用帶有參數(shù)的 toArray 方法,這時(shí)就有一個(gè)坑,如果參數(shù)數(shù)組的大小不夠,這時(shí)候返回的數(shù)組值是空。
「Collections.emptyList()方法」
問題:
在返回的 Collections.emptyList(); 上調(diào)用了add()方法,拋出異常 UnsupportedOperationException。
分析:
Collections.emptyList() 返回的是不可變的空列表,這個(gè)空列表對(duì)應(yīng)的類型是EmptyList,這個(gè)類是Collections中的靜態(tài)內(nèi)部類,繼承了AbstractList。
AbstractList中默認(rèn)的add方法是沒有實(shí)現(xiàn)的,直接拋出UnsupportedOperationException異常。
而EmptyList只是繼承了AbstractList,卻并沒有重寫add方法,因此直接調(diào)用add方法會(huì)拋異常。
除了emptyList,還有emptySet、emptyMap等也一樣。
「List.subList()方法」
list.subList() 產(chǎn)生的集合也會(huì)與原始List互相影響。
建議使用時(shí),通過List list = Lists.newArrayList(arrays); 來生成一個(gè)新的list,不要再操作原列表。
「UnmodifiableList」
UnmodifiableList是Collections中的內(nèi)部類,通過調(diào)用 Collections.unmodifiableList(List list) 可返回指定集合的不可變集合。
集合只能被讀取,不能做任何增刪改操作,從而保護(hù)不可變集合的安全。但這個(gè)不可變僅僅是正向的不可變。
反過來如果修改了原來的集合,則這個(gè)不可變集合仍會(huì)被同步修改。因?yàn)椴豢勺兗系讓邮褂玫倪€是原來的List。
Map?
「ConcurrentHashMap不允許為null」
ConcurrentHashMap#put?方法的源碼,開頭就看到了對(duì)KV的判空校驗(yàn)。
為什么ConcurrentHashMap?與 HashMap設(shè)計(jì)的判斷邏輯不一樣?
Doug Lea 老爺子的解釋是:
- null?會(huì)引起歧義,如果value為null?,我們無法得知是值為null?,還是key未映射具體值?
- Doug Lea 并不喜歡null?,認(rèn)為null 就是個(gè)隱藏的炸彈。
貼一下常用Map?子類集合對(duì)于 null存儲(chǔ)情況:
「HashMap 是無序的」
舉例:
import java.util.HashMap;
public class App {
public static void main(String[] args) {
HashMap<String, Object> result = getList();
result.forEach((k, v) -> {
System.out.println(k + ":" + v);
});
}
// 查詢方法(簡(jiǎn)化版)
public static HashMap<String, Object> getList() {
HashMap<String, Object> result = new HashMap<>(); // 最終返回的結(jié)果集
// 偽代碼:從數(shù)據(jù)庫(kù)中查詢出了數(shù)據(jù),然后對(duì)數(shù)據(jù)進(jìn)行處理之后,存到了
for (int i = 1; i <= 5; i++) {
result.put("2022-" + i, "hello java" + i);
}
return result;
}
}
結(jié)果并沒有按先后順序返回。
原因分析
HashMap 使用的是哈希方式進(jìn)行存儲(chǔ)的,因此存入和讀取的順序可能是不一致的,這也說 HashMap 是無序的集合,所以會(huì)導(dǎo)致插入的順序,與最終展示的順序不一致。
解決方案:將無序的 HashMap 改為有序的 LinkedHashMap。
LinkedHashMap 屬于 HashMap 的子類,所以 LinkedHashMap 除了擁有 HashMap 的所有特性之后,還具備自身的一些擴(kuò)展屬性,其中就包括 LinkedHashMap 中額外維護(hù)了一個(gè)雙向鏈表,這個(gè)雙向鏈表就是用來保存元素的(插入)順序的。
Set?
如果是需要對(duì)我們自定義的對(duì)象去重,就需要我們重寫 hashCode 和 equals 方法。
不然HashSet調(diào)用默認(rèn)的hashCode方法判斷對(duì)象的地址,不等就達(dá)不到想根據(jù)對(duì)象的值去重的目的。