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