## 14 簡化工作:Guava Lists Maps 實際工作運用和源碼
## 引導語
在日常工作中,我們經常會使用一些三方的 API 來簡化我們的工作,Guava 就是其中一種,Guava 是 Google 開源的技術框架,使用率高,社區活躍度也很高。
本小節我們從工作中對 Guava 集合的使用入手,然后深入的看下其底層的實現,最后總結其設計思想,感興趣的同學也可以下載源碼學習,GitHub 地址:https://github.com/google/guava,源碼中 guava 的文件夾為其源碼。
### 1 運用工廠模式進行初始化
在集合類初始化方面,Guava 比 Java 原生的 API 更加好用,還發明了很多新的功能,比如說在 JDK 7 之前,我們新建集合類時,聲明和初始化都必須寫上泛型說明,像這樣:List<泛型> list = new ArrayList<泛型>(); , JDK 7 之后有所改變,我們只需要在聲明處寫上泛型說明,像這樣:List<泛型> list = new ArrayList<>();。
Guava 提供了更加方便的使用姿勢,采用了工廠模式,把集合創建的邏輯交給了工廠,開發者無需關注工廠底層是如何創建的,只需要關心,工廠能產生什么,代碼于是變成了這樣:List<泛型> list = Lists.newArrayList();,Lists 就是 Guava 提供出來的,方便操作 List 的工具類。
這種寫法其實就是一種簡單的工廠模式,只需要定義好工廠的入參和出參,就能對外隱藏其內部的創建邏輯,提供更加方便的使用體驗。
當然除了 Lists,Guava 還提供了很多其他實用工具,如 Maps、Sets,接下來我們分別來看下這些常用工具的使用和原理。
### 2 Lists
#### 2.1 初始化
Lists 最大的功能是能幫助我們進行 List 的初始化,比如我們剛說的 newArrayList 這種:
```
List<String> list = Lists.newArrayList(); public static <E> ArrayList<E> newArrayList() { return new ArrayList<>(); } // 這種底層是幫助我們寫好了泛型,E 代表泛型,表示當前返回的泛型類型和聲明的一致即可,在編譯的時候,會把泛型 E 轉化成我們聲明的 String。
```
```
如果你清楚 List 的大小,我們也可以這樣做:
// 可以預估 list 的大小為 20 List<String> list = Lists.newArrayListWithCapacity(20); // 不太肯定 list 大小是多少,但期望是大小是 20 上下。 List<String> list = Lists.newArrayListWithExpectedSize(20);
```
newArrayListWithCapacity(20) 方法內部實現是:new ArrayList<>(20);,而 newArrayListWithExpectedSize 方法內部實現是對 List 大小有一個計算公式的,計算公式為:5L + arraySize + (arraySize / 10) ,arraySize 表示傳進來的值,公式簡化下就是 5 + 11/10 * arraySize,因為這個方法表示期望的大小,所以這里取的約是期望值的十分之十一,比傳進來的值約大十分之一,所以根據 20 最終計算出來的值是 27。
Lists 在初始化的時候,還支持傳迭代器的入參(只適合小數據量的迭代器的入參),源碼如下:
```
public static <E> ArrayList<E> newArrayList(Iterator<? extends E> elements) { ArrayList<E> list = newArrayList(); // addAll 方法底層其實通過迭代器進行 for 循環添加 Iterators.addAll(list, elements); return list; }
```
從 Lists 對 List 初始化進行包裝的底層源碼來看,底層源碼非常簡單的,但我們還是愿意使用這種方式的包裝,主要是因為這種工廠模式的包裝,使我們的使用姿勢更加優雅,使用起來更加方便。
#### 2.2 分組和反轉排序
除了初始化之外,Lists 還提供了兩個比較實用的功能,分組和反轉排序功能,我們分別來演示一下:
```
// 演示反轉排序 public void testReverse(){ List<String> list = new ArrayList<String>(){{ add("10"); add("20"); add("30"); add("40"); }}; log.info("反轉之前:"+JSON.toJSONString(list)); list = Lists.reverse(list); log.info("反轉之后:"+JSON.toJSONString(list)); } // 打印出來的結果為:
反轉之前:["10","20","30","40"] 反轉之后:["40","30","20","10"]
```
reverse 方法底層實現非常巧妙,底層覆寫了 List 原生的 get(index) 方法,會把傳進來的 index 進行 (size - 1) - index 的計算,使計算得到的索引位置和 index 位置正好相反,這樣當我們 get 時,數組索引位置的 index 已經是相反的位置了,達到了反轉排序的效果,其實底層并沒有進行反轉排序,只是在計算相反的索引位置,通過計算相反的索引位置這樣簡單的設計,得到了反轉排序的效果,很精妙。
在工作中,有時候我們需要把一個大的 list 進行切分,然后再把每份丟給線程池去運行,最后將每份運行的結果匯總,Lists 工具類就提供了一個對 list 進行切分分組的方法,演示 demo 如下:
```
// 分組 public void testPartition(){ List<String> list = new ArrayList<String>(){{ add("10"); add("20"); add("30"); add("40"); }}; log.info("分組之前:"+JSON.toJSONString(list)); List<List<String>> list2 = Lists.partition(list,3); log.info("分組之后:"+JSON.toJSONString(list2)); } 輸出結果為: 分組之前:["10","20","30","40"] 分組之后:[["10","20","30"],["40"]]
```
partition 方法的第二個參數的意思,你想讓分組后的 List 包含幾個元素,這個方法的底層實現其實就是 subList 方法。
有一點需要我們注意的是這兩個方法返回的 List 并不是 ArrayList,是自定義的 List,所以對于 ArrayList 的有些功能可能并不支持,使用的時候最好能看下源碼,看看底層有無支持。
#### 2.3 小結
Lists 上述的方法大大的方便了我們進行開發,簡化了使用姿勢,但其內部實現卻非常簡單巧妙,比如說 reverse 方法可以輸出相反排序的 List,但底層并沒有實現排序,只是計算了索引位置的相反值而已,這點值得我們學習。
### 3 Maps
#### 3.1 初始化
Maps 也是有著各種初始化 Map 的各種方法,原理不說了,和 Lists 類似,我們演示下如何使用:
```
Map<String,String> hashMap = Maps.newHashMap(); Map<String,String> linkedHashMap = Maps.newLinkedHashMap(); // 這里 Map 的初始化大小公式和 HashSet 初始化公式類似,還記得 HashSet 初始化 HashMap 時,經典的計算初始大小的公式么:取最大值(期望的值 / 0.75 + 1,默認值 16),newHashMapWithExpectedSize 方法底層也是這么算的初始化大小的 Map<String,String> withExpectedSizeHashMap = Maps.newHashMapWithExpectedSize(20);
```
#### 3.2 difference
Maps 提供了一個特別有趣也很實用的方法:difference,此方法的目的是比較兩個 Map 的差異,入參就是兩個 Map,比較之后能夠返回四種差異:
1. 左邊 Map 獨有 key。
2. 右邊 Map 獨有 key。
3. 左右邊 Map 都有 key,并且 value 相等。
4. 左右邊 Map 都有 key,但是 value 不等。
我們用代碼來演示一下:
```
// ImmutableMap.of 也是 Guava 提供初始化 Map 的方法,入參格式為 k1,v1,k2,v2,k3,v3…… Map<String,String> leftMap = ImmutableMap.of("1","1","2","2","3","3"); Map<String,String> rightMap = ImmutableMap.of("2","2","3","30","4","4"); MapDifference difference = Maps.difference(leftMap, rightMap); log.info("左邊 map 獨有 key:{}",difference.entriesOnlyOnLeft()); log.info("右邊 map 獨有 key:{}",difference.entriesOnlyOnRight()); log.info("左右邊 map 都有 key,并且 value 相等:{}",difference.entriesInCommon()); log.info("左右邊 map 都有 key,但 value 不等:{}",difference.entriesDiffering());
```
最后打印結果為:
```
左邊 map 獨有 key:{1=1} 右邊 map 獨有 key:{4=4} 左右邊 map 都有 key,并且 value 相等:{2=2} 左右邊 map 都有 key,但 value 不等:{3=(3, 30)}
```
從這個 demo 我們可以看到此方法的強大威力,我們在工作中經常遇到 Map 或者 List 間比較差異的任務,我們就可以直接使用該方法進行對比,List 可以先轉化成 Map。
而且 difference 底層的實現也算是最優的實現了,只需要循環一遍,就可得到上述四種差異結果,源碼解析如下:
```
// 對比兩個 map 的差異 private static <K, V> void doDifference( Map<? extends K, ? extends V> left, Map<? extends K, ? extends V> right, Equivalence<? super V> valueEquivalence, // key 只在左邊 map 出現 Map<K, V> onlyOnLeft, // key 只在右邊 map 出現,調用 doDifference 方法前已經包含了全部右邊的值 Map<K, V> onlyOnRight,
// key 在左右 map 中都出現過,并且 value 都相等 Map<K, V> onBoth, // key 在左右 map 中都出現過,但 value 不等 Map<K, MapDifference.ValueDifference<V>> differences) { // 以左邊 map 為基準進行循環 for (Entry<? extends K, ? extends V> entry : left.entrySet()) { K leftKey = entry.getKey(); V leftValue = entry.getValue(); // 右邊 map 包含左邊的 key if (right.containsKey(leftKey)) { // onlyOnRight 已經包含全部右邊的值 所以需要刪除當前 key V rightValue = onlyOnRight.remove(leftKey); // key 相等,并且 value 值也相等 if (valueEquivalence.equivalent(leftValue, rightValue)) { onBoth.put(leftKey, leftValue); // key 相等,但 value 值不等 } else { differences.put(leftKey, ValueDifferenceImpl.create(leftValue, rightValue)); } // 右邊 map 不包含左邊的 key,就是左邊 map 獨有的 key } else { onlyOnLeft.put(leftKey, leftValue); } } }
```
這是一種比較優秀的,快速比對的算法,可以好好看下上面的源碼,然后把這種算法背下來,或者自己再次實現一次。
Sets 的使用方式和 Lists 和 Maps 很類似,沒有太大的亮點,我們就不說了。
### 4 總結
這一小節主要都是實戰內容,在實際工作中可以用起來。
在 Guava 對集合的設計中,有兩個大點是非常值得我們學習的:
1. Lists、Maps 的出現給我們提供了更方便的使用姿勢和方法,我們在實際工作中,如果碰到特別繁瑣,或者特別難用的 API,我們也可以進行一些包裝,使更好用,這個是屬于在解決目前的痛點的問題上進行創新,是非常值得提倡的一件事情,往往可以幫助你拿到更好的績效。
2. 如果有人問你,List 或者 Map 高效的差異排序算法,完全可以參考 Maps.difference 的內部實現,該方法只使用了一次循環,就可得到所有的相同或不同結果,這種算法在我們工作中也經常被使用。
了解更多,可以直接前往 Guava 的代碼庫查看:https://github.com/google/guava
- 前言
- 第1章 基礎
- 01 開篇詞:為什么學習本專欄
- 02 String、Long 源碼解析和面試題
- 03 Java 常用關鍵字理解
- 04 Arrays、Collections、Objects 常用方法源碼解析
- 第2章 集合
- 05 ArrayList 源碼解析和設計思路
- 06 LinkedList 源碼解析
- 07 List 源碼會問哪些面試題
- 08 HashMap 源碼解析
- 09 TreeMap 和 LinkedHashMap 核心源碼解析
- 10 Map源碼會問哪些面試題
- 11 HashSet、TreeSet 源碼解析
- 12 彰顯細節:看集合源碼對我們實際工作的幫助和應用
- 13 差異對比:集合在 Java 7 和 8 有何不同和改進
- 14 簡化工作:Guava Lists Maps 實際工作運用和源碼
- 第3章 并發集合類
- 15 CopyOnWriteArrayList 源碼解析和設計思路
- 16 ConcurrentHashMap 源碼解析和設計思路
- 17 并發 List、Map源碼面試題
- 18 場景集合:并發 List、Map的應用場景
- 第4章 隊列
- 19 LinkedBlockingQueue 源碼解析
- 20 SynchronousQueue 源碼解析
- 21 DelayQueue 源碼解析
- 22 ArrayBlockingQueue 源碼解析
- 23 隊列在源碼方面的面試題
- 24 舉一反三:隊列在 Java 其它源碼中的應用
- 25 整體設計:隊列設計思想、工作中使用場景
- 26 驚嘆面試官:由淺入深手寫隊列
- 第5章 線程
- 27 Thread 源碼解析
- 28 Future、ExecutorService 源碼解析
- 29 押寶線程源碼面試題
- 第6章 鎖
- 30 AbstractQueuedSynchronizer 源碼解析(上)
- 31 AbstractQueuedSynchronizer 源碼解析(下)
- 32 ReentrantLock 源碼解析
- 33 CountDownLatch、Atomic 等其它源碼解析
- 34 只求問倒:連環相扣系列鎖面試題
- 35 經驗總結:各種鎖在工作中使用場景和細節
- 36 從容不迫:重寫鎖的設計結構和細節
- 第7章 線程池
- 37 ThreadPoolExecutor 源碼解析
- 38 線程池源碼面試題
- 39 經驗總結:不同場景,如何使用線程池
- 40 打動面試官:線程池流程編排中的運用實戰
- 第8章 Lambda 流
- 41 突破難點:如何看 Lambda 源碼
- 42 常用的 Lambda 表達式使用場景解析和應用
- 第9章 其他
- 43 ThreadLocal 源碼解析
- 44 場景實戰:ThreadLocal 在上下文傳值場景下的實踐
- 45 Socket 源碼及面試題
- 46 ServerSocket 源碼及面試題
- 47 工作實戰:Socket 結合線程池的使用
- 第10章 專欄總結
- 48 一起看過的 Java 源碼和面試真題