1. 程式人生 > >老哥,您看我這篇Java集合,還有機會評優嗎?

老哥,您看我這篇Java集合,還有機會評優嗎?

集合在我們日常開發使用的次數數不勝數,`ArrayList`/`LinkedList`/`HashMap`/`HashSet`······信手拈來,抬手就拿來用,在 IDE 上龍飛鳳舞,但是作為一名合格的優雅的程式猿,僅僅瞭解怎麼使用`API`是遠遠不夠的,如果在呼叫`API`時,知道它內部發生了什麼事情,就像開了`透視`外掛一樣,洞穿一切,這種感覺才真的爽,而且這樣就**不是集合提供什麼功能給我們使用,而是我們選擇使用它的什麼功能了**。 ![img](https://cdn.nlark.com/yuque/0/2020/png/1694029/1596208672973-ae64a99f-3831-4b18-b4f4-c69237eba98a.png?x-oss-process=image%2Fresize%2Cw_2400) ## 集合框架總覽 下圖堪稱集合框架的**上帝視角**,講到集合框架不得不看的就是這幅圖,當然,你會覺得眼花繚亂,不知如何看起,這篇文章帶你一步一步地秒殺上面的每一個介面、抽象類和具體類。我們將會從最頂層的介面開始講起,一步一步往下深入,幫助你把對集合的認知構建起一個知識網路。 ![collection.jpeg](https://cdn.nlark.com/yuque/0/2020/jpeg/1694029/1596110252275-7ae0c7a7-b8a9-4e36-bc2f-d651c6e0ae23.jpeg) 工欲善其事必先利其器,讓我們先來過一遍整個集合框架的組成部分: 1. 集合框架提供了兩個遍歷介面:`Iterator`和`ListIterator`,其中後者是前者的`優化版`,支援在任意一個位置進行**前後雙向遍歷**。注意圖中的`Collection`應當繼承的是`Iterable`而不是`Iterator`,後面會解釋`Iterable`和`Iterator`的區別 2. 整個集合框架分為兩個門派(型別):`Collection`和`Map`,前者是一個容器,儲存一系列的**物件**;後者是鍵值對``,儲存一系列的**鍵值對** 3. 在集合框架體系下,衍生出四種具體的集合型別:`Map`、`Set`、`List`、`Queue` 4. `Map`儲存``鍵值對,查詢元素時通過`key`查詢`value` 5. `Set`內部儲存一系列**不可重複**的物件,且是一個**無序**集合,物件排列順序不一 6. `List`內部儲存一系列**可重複**的物件,是一個**有序**集合,物件按插入順序排列 7. `Queue`是一個**佇列**容器,其特性與`List`相同,但只能從`隊頭`和`隊尾`操作元素 8. JDK 為集合的各種操作提供了兩個工具類`Collections`和`Arrays`,之後會講解工具類的常用方法 9. 四種抽象集合型別內部也會衍生出許多具有不同特性的集合類,**不同場景下擇優使用,沒有最佳的集合** 上面瞭解了整個集合框架體系的組成部分,接下來的章節會嚴格按照上面羅列的順序進行講解,每一步都會有`承上啟下`的作用 > 學習`Set`前,最好最好要先學習`Map`,因為`Set`的操作本質上是對`Map`的操作,往下看準沒錯 ### Iterator Iterable ListIterator 在第一次看這兩個介面,真以為是一模一樣的,沒發現裡面有啥不同,**存在即合理**,它們兩個還是有本質上的區別的。 首先來看`Iterator`介面: ```java public interface Iterator { boolean hasNext(); E next(); void remove(); } ``` 提供的API介面含義如下: - `hasNext()`:判斷集合中是否存在下一個物件 - `next()`:返回集合中的下一個物件,並將訪問指標移動一位 - `remove()`:刪除集合中呼叫`next()`方法返回的物件 在早期,遍歷集合的方式只有一種,通過`Iterator`迭代器操作 ```java List list = new ArrayList<>(); list.add(1); list.add(2); list.add(3); Iterator iter = list.iterator(); while (iter.hasNext()) { Integer next = iter.next(); System.out.println(next); if (next == 2) { iter.remove(); } } ``` 再來看`Iterable`介面: ```java public interface Iterable { Iterator iterator(); // JDK 1.8 default void forEach(Consumer action) { Objects.requireNonNull(action); for (T t : this) { action.accept(t); } } } ``` 可以看到`Iterable`接口裡面提供了`Iterator`介面,所以實現了`Iterable`介面的集合依舊可以使用`迭代器`遍歷和操作集合中的物件; 而在 `JDK 1.8`中,`Iterable`提供了一個新的方法`forEach()`,它允許使用增強 for 迴圈遍歷物件。 ```java List list = new ArrayList<>(); for (Integer num : list) { System.out.println(num); } ``` 我們通過命令:`javap -c`反編譯上面的這段程式碼後,發現它只是 Java 中的一個`語法糖`,本質上還是呼叫`Iterator`去遍歷。 ![image-20200729000858377.png](https://cdn.nlark.com/yuque/0/2020/png/1694029/1596110168937-9464cc3c-9198-4ef0-ada3-97c86cfaf175.png) 翻譯成程式碼,就和一開始的`Iterator`迭代器遍歷方式基本相同了。 ```java Iterator iter = list.iterator(); while (iter.hasNext()) { Integer num = iter.next(); System.out.println(num); } ``` > 還有更深層次的探討:為什麼要設計兩個介面`Iterable`和`Iterator`,而不是保留其中一個就可以了。 > > 簡單講解:`Iterator`的保留可以讓子類去**實現自己的迭代器**,而`Iterable`介面更加關注於`for-each`的增強語法。具體可參考:[Java中的Iterable與Iterator詳解](https://www.cnblogs.com/litexy/p/9744241.html) 關於`Iterator`和`Iterable`的講解告一段落,下面來總結一下它們的重點: 1. `Iterator`是提供集合操作內部物件的一個迭代器,它可以**遍歷、移除**物件,且只能夠**單向移動** 2. `Iterable`是對`Iterator`的封裝,在`JDK 1.8`時,實現了`Iterable`介面的集合可以使用**增強 for 迴圈**遍歷集合物件,我們通過**反編譯**後發現底層還是使用`Iterator`迭代器進行遍歷 等等,這一章還沒完,還有一個`ListIterator`。它繼承 Iterator 介面,在遍歷`List`集合時可以從**任意索引下標**開始遍歷,而且支援**雙向遍歷**。 ListIterator 存在於 List 集合之中,通過呼叫方法可以返回**起始下標**為 `index`的迭代器 ```java List list = new ArrayList<>(); // 返回下標為0的迭代器 ListIterator listIter1 = list.listIterator(); // 返回下標為5的迭代器 ListIterator listIter2 = list.listIterator(5); ``` ListIterator 中有幾個重要方法,大多數方法與 Iterator 中定義的含義相同,但是比 Iterator 強大的地方是可以在**任意一個下標位置**返回該迭代器,且可以實現**雙向遍歷**。 ```java public interface ListIterator extends Iterator { boolean hasNext(); E next(); boolean hasPrevious(); E previous(); int nextIndex(); int previousIndex(); void remove(); // 替換當前下標的元素,即訪問過的最後一個元素 void set(E e); void add(E e); } ``` ### Map 和 Collection 介面 Map 介面和 Collection 介面是集合框架體系的兩大門派,Collection 是儲存元素本身,而 Map 是儲存``鍵值對,在 Collection 門派下有一小部分弟子去`偷師`,利用 Map 門派下的弟子來修煉自己。 是不是聽的一頭霧水哈哈哈,舉個例子你就懂了:`HashSet`底層利用了`HashMap`,`TreeSet`底層用了`TreeMap`,`LinkedHashSet`底層用了`LinkedHashMap`。 下面我會詳細講到各個具體集合類哦,所以在這裡,我們先從整體上了解這兩個`門派`的特點和區別。 ![img](https://cdn.nlark.com/yuque/0/2020/png/1694029/1595998378619-e54eb2d1-128a-448e-98c6-4941d81546b5.png) `Map`介面定義了儲存的資料結構是``形式,根據 key 對映到 value,一個 key 對應一個 value ,所以`key`不可重複,而`value`可重複。 在`Map`介面下會將儲存的方式細分為不同的種類: - `SortedMap`介面:該類對映可以對``按照自己的規則進行**排序**,具體實現有 TreeMap - `AbsractMap`:它為子類提供好一些**通用的API實現**,所有的具體Map如`HashMap`都會繼承它 而`Collection`介面提供了所有集合的**通用方法**(注意這裡不包括`Map`): - 新增方法:`add(E e)` / `addAll(Collection var1)` - 刪除方法:`remove(Object var1)` / `removeAll(Collection var1)` - 查詢方法:`contains(Object var1)` / `containsAll(Collection var1);` - 查詢集合自身資訊:`size()` / `isEmpty()` - ··· 在`Collection`介面下,同樣會將集合細分為不同的種類: - `Set`介面:一個**不允許儲存重複元素**的**無序**集合,具體實現有`HashSet` / `TreeSet`··· - `List`介面:一個**可儲存重複元素**的**有序**集合,具體實現有`ArrayList` / `LinkedList`··· - `Queue`介面:一個**可儲存重複元素**的**佇列**,具體實現有`PriorityQueue` / `ArrayDeque`··· ## Map 集合體系詳解 `Map`介面是由``組成的集合,由`key`對映到**唯一**的`value`,所以`Map`不能包含重複的`key`,每個鍵**至多**對映一個值。下圖是整個 Map 集合體系的主要組成部分,我將會按照日常使用頻率從高到低一一講解。 不得不提的是 Map 的設計理念:**定位元素**的時間複雜度優化到 `O(1)` Map 體系下主要分為 AbstractMap 和 SortedMap兩類集合 `AbstractMap`是對 Map 介面的擴充套件,它定義了普通的 Map 集合具有的**通用行為**,可以避免子類重複編寫大量相同的程式碼,子類繼承 AbstractMap 後可以重寫它的方法,**實現額外的邏輯**,對外提供更多的功能。 `SortedMap` 定義了該類 Map 具有 `排序`行為,同時它在內部定義好有關排序的抽象方法,當子類實現它時,必須重寫所有方法,對外提供排序功能。
### HashMap HashMap 是一個**最通用的**利用雜湊表儲存元素的集合,將元素放入 HashMap 時,將`key`的雜湊值轉換為陣列的`索引`下標**確定存放位置**,查詢時,根據`key`的雜湊地址轉換成陣列的`索引`下標**確定查詢位置**。 HashMap 底層是用陣列 + 連結串列 + 紅黑樹這三種資料結構實現,它是**非執行緒安全**的集合。 ![img](https://cdn.nlark.com/yuque/0/2020/png/1694029/1595419821508-67e5c9e1-ab2a-4f96-9df7-7a37658e564a.png) 傳送雜湊衝突時,HashMap 的解決方法是將相同對映地址的元素連成一條`連結串列`,如果連結串列的長度大於`8`時,且陣列的長度大於`64`則會轉換成`紅黑樹`資料結構。 關於 HashMap 的簡要總結: 1. 它是集合中最常用的`Map`集合型別,底層由`陣列 + 連結串列 + 紅黑樹`組成 2. HashMap不是執行緒安全的 3. 插入元素時,通過計算元素的`雜湊值`,通過**雜湊對映函式**轉換為`陣列下標`;查詢元素時,同樣通過雜湊對映函式得到陣列下標`定位元素的位置` ### LinkedHashMap LinkedHashMap 可以看作是 `HashMap` 和 `LinkedList` 的結合:它在 HashMap 的基礎上添加了一條雙向連結串列,`預設`儲存各個元素的插入順序,但由於這條雙向連結串列,使得 LinkedHashMap 可以實現 `LRU`快取淘汰策略,因為我們可以設定這條雙向連結串列按照`元素的訪問次序`進行排序 ![img](https://cdn.nlark.com/yuque/0/2020/png/1694029/1596103017691-7eb35af1-3fde-46b8-aa56-31df67c1b3de.png) LinkedHashMap 是 HashMap 的子類,所以它具備 HashMap 的所有特點,其次,它在 HashMap 的基礎上維護了一條`雙向連結串列`,該連結串列儲存了**所有元素**,`預設`元素的順序與插入順序**一致**。若`accessOrder`屬性為`true`,則遍歷順序按元素的訪問次序進行排序。 ```java // 頭節點 transient LinkedHashMap.Entry head; // 尾結點 transient LinkedHashMap.Entry tail; ``` 利用 LinkedHashMap 可以實現 `LRU` 快取淘汰策略,因為它提供了一個方法: ```java protected boolean removeEldestEntry(java.util.Map.Entry eldest) { return false; } ``` 該方法可以移除`最靠近連結串列頭部`的一個節點,而在`get()`方法中可以看到下面這段程式碼,其作用是挪動結點的位置: ```java if (this.accessOrder) { this.afterNodeAccess(e); } ``` 只要呼叫了`get()`且`accessOrder = true`,則會將該節點更新到連結串列`尾部`,具體的邏輯在`afterNodeAccess()`中,感興趣的可翻看原始碼,篇幅原因這裡不再展開。 現在如果要實現一個`LRU`快取策略,則需要做兩件事情: - 指定`accessOrder = true`可以設定連結串列按照訪問順序排列,通過提供的構造器可以設定`accessOrder` ```java public LinkedHashMap(int initialCapacity, float loadFactor, boolean accessOrder) { super(initialCapacity, loadFactor); this.accessOrder = accessOrder; } ``` - 重寫`removeEldestEntry()`方法,內部定義邏輯,通常是判斷`容量`是否達到上限,若是則執行淘汰。 這裡就要貼出一道大廠面試必考題目:[146. LRU快取機制](https://leetcode-cn.com/problems/lru-cache/),只要跟著我的步驟,就能順利完成這道大廠題了。 關於 LinkedHashMap 主要介紹兩點: 1. 它底層維護了一條`雙向連結串列`,因為繼承了 HashMap,所以它也不是執行緒安全的 2. LinkedHashMap 可實現`LRU`快取淘汰策略,其原理是通過設定`accessOrder`為`true`並重寫`removeEldestEntry`方法定義淘汰元素時需滿足的條件 ### TreeMap TreeMap 是 `SortedMap` 的子類,所以它具有**排序**功能。它是基於`紅黑樹`資料結構實現的,每一個鍵值對``都是一個結點,預設情況下按照`key`自然排序,另一種是可以通過傳入定製的`Comparator`進行自定義規則排序。 ```java // 按照 key 自然排序,Integer 的自然排序是升序 TreeMap naturalSort = new TreeMap<>(); // 定製排序,按照 key 降序排序 TreeMap customSort = new TreeMap<>((o1, o2) -> Integer.compare(o2, o1)); ``` TreeMap 底層使用了陣列+紅黑樹實現,所以裡面的儲存結構可以理解成下面這幅圖哦。 ![image-20200730180101883.png](https://cdn.nlark.com/yuque/0/2020/png/1694029/1596103385086-d684f26e-dabd-44b7-bab8-151af172496a.png) 圖中紅黑樹的每一個節點都是一個`Entry`,在這裡為了圖片的簡潔性,就不標明 key 和 value 了,注意這些元素都是已經按照`key`排好序了,整個資料結構都是保持著`有序` 的狀態! 關於`自然`排序與`定製`排序: - 自然排序:要求`key`必須實現`Comparable`介面。 由於`Integer`類實現了 Comparable 介面,按照自然排序規則是按照`key`從小到大排序。 ```java TreeMap treeMap = new TreeMap<>(); treeMap.put(2, "TWO"); treeMap.put(1, "ONE"); System.out.print(treeMap); // {1=ONE, 2=TWO} ``` - 定製排序:在初始化 TreeMap 時傳入新的`Comparator`,**不**要求`key`實現 Comparable 介面 ```java TreeMap treeMap = new TreeMap<>((o1, o2) -> Integer.compare(o2, o1)); treeMap.put(1, "ONE"); treeMap.put(2, "TWO"); treeMap.put(4, "FOUR"); treeMap.put(3, "THREE"); System.out.println(treeMap); // {4=FOUR, 3=THREE, 2=TWO, 1=ONE} ``` 通過傳入新的`Comparator`比較器,可以覆蓋預設的排序規則,上面的程式碼按照`key`降序排序,在實際應用中還可以按照其它規則自定義排序。 `compare()`方法的返回值有三種,分別是:`0`,`-1`,`+1` (1)如果返回`0`,代表兩個元素相等,不需要調換順序 (2)如果返回`+1`,代表前面的元素需要與後面的元素調換位置 (3)如果返回`-1`,代表前面的元素不需要與後面的元素調換位置 而何時返回`+1`和`-1`,則由我們自己去定義,JDK預設是按照**自然排序**,而我們可以根據`key`的不同去定義降序還是升序排序。 關於 TreeMap 主要介紹了兩點: 1. 它底層是由`紅黑樹`這種資料結構實現的,所以操作的時間複雜度恆為`O(logN)` 2. TreeMap 可以對`key`進行自然排序或者自定義排序,自定義排序時需要傳入`Comparator`,而自然排序要求`key`實現了`Comparable`介面 3. TreeMap 不是執行緒安全的。 ### WeakHashMap WeakHashMap 日常開發中比較少見,它是基於普通的`Map`實現的,而裡面`Entry`中的鍵在每一次的`垃圾回收`都會被清除掉,所以非常適合用於**短暫訪問、僅訪問一次**的元素,快取在`WeakHashMap`中,並儘早地把它回收掉。 當`Entry`被`GC`時,WeakHashMap 是如何感知到某個元素被回收的呢? 在 WeakHashMap 內部維護了一個引用佇列`queue` ```java private final ReferenceQueue queue = new ReferenceQueue<>(); ``` 這個 queue 裡包含了所有被`GC`掉的鍵,當JVM開啟`GC`後,如果回收掉 WeakHashMap 中的 key,會將 key 放入queue 中,在`expungeStaleEntries()`中遍歷 queue,把 queue 中的所有`key`拿出來,並在 WeakHashMap 中刪除掉,以達到**同步**。 ```java private void expungeStaleEntries() { for (Object x; (x = queue.poll()) != null; ) { synchronized (queue) { // 去 WeakHashMap 中刪除該鍵值對 } } } ``` 再者,需要注意 WeakHashMap 底層儲存的元素的資料結構是`陣列 + 連結串列`,**沒有紅黑樹**哦,可以換一個角度想,如果還有紅黑樹,那乾脆直接繼承 HashMap ,然後再擴充套件就完事了嘛,然而它並沒有這樣做: ```java public class WeakHashMap extends AbstractMap implements Map { } ``` 所以,WeakHashMap 的資料結構圖我也為你準備好啦。 ![image.png](https://cdn.nlark.com/yuque/0/2020/png/1694029/1596106079292-a74fb47e-bb54-47e2-81ac-1254428e73b7.png) 圖中被虛線標識的元素將會在下一次訪問 WeakHashMap 時被刪除掉,WeakHashMap 內部會做好一系列的調整工作,所以記住佇列的作用就是標誌那些已經被`GC`回收掉的元素。 關於 WeakHashMap 需要注意兩點: 1. 它的鍵是一種**弱鍵**,放入 WeakHashMap 時,隨時會被回收掉,所以不能確保某次訪問元素一定存在 2. 它依賴普通的`Map`進行實現,是一個非執行緒安全的集合 3. WeakHashMap 通常作為**快取**使用,適合儲存那些**只需訪問一次**、或**只需儲存短暫時間**的鍵值對 ### Hashtable Hashtable 底層的儲存結構是`陣列 + 連結串列`,而它是一個**執行緒安全**的集合,但是因為這個執行緒安全,它就被淘汰掉了。 下面是Hashtable儲存元素時的資料結構圖,它只會存在陣列+連結串列,當連結串列過長時,查詢的效率過低,而且會長時間**鎖住** Hashtable。 ![image.png](https://cdn.nlark.com/yuque/0/2020/png/1694029/1596106328540-e1acec81-7896-45be-85c4-c93e142ef610.png) > 這幅圖是否有點眼熟哈哈哈哈,本質上就是 WeakHashMap 的底層儲存結構了。你千萬別問為什麼 WeakHashMap 不繼承 Hashtable 哦,Hashtable 的`效能`在併發環境下非常差,在非併發環境下可以用`HashMap`更優。 HashTable 本質上是 HashMap 的前輩,它被淘汰的原因也主要因為兩個字:**效能** HashTable 是一個 **執行緒安全** 的 Map,它所有的方法都被加上了 **synchronized** 關鍵字,也是因為這個關鍵字,它註定成為了時代的棄兒。 HashTable 底層採用 **陣列+連結串列** 儲存鍵值對,由於被棄用,後人也沒有對它進行任何改進 HashTable 預設長度為 `11`,負載因子為 `0.75F`,即元素個數達到陣列長度的 75% 時,會進行一次擴容,每次擴容為原來陣列長度的 `2` 倍 HashTable 所有的操作都是執行緒安全的。 ## Collection 集合體系詳解 Collection 集合體系的頂層介面就是`Collection`,它規定了該集合下的一系列行為約定。 該集合下可以分為三大類集合:List,Set和Queue `Set`介面定義了該類集合**不允許儲存重複**的元素,且任何操作時均需要通過**雜湊函式對映**到集合內部定位元素,集合內部的元素**預設**是**無序**的。 `List`介面定義了該類集合**允許儲存重複**的元素,且集合內部的元素按照元素插入的順序**有序排列**,可以通過**索引**訪問元素。 `Queue`介面定義了該類集合是以`佇列`作為儲存結構,所以集合內部的元素**有序排列**,僅可以操作**頭結點**元素,無法訪問佇列中間的元素。 上面三個介面是**最普通,最抽象**的實現,而在各個集合介面內部,還會有更加具體的表現,衍生出各種不同的**額外功能**,使開發者能夠對比各個集合的優勢,**擇優使用**。 ![image.png](https://cdn.nlark.com/yuque/0/2020/png/1694029/1595682008755-6f1e6c3c-920e-427b-9c00-a4164026f181.png) ### Set 介面 `Set`介面繼承了`Collection`介面,是一個不包括重複元素的集合,更確切地說,Set 中任意兩個元素不會出現 `o1.equals(o2)`,而且 Set **至多**只能儲存一個 `NULL` 值元素,Set 集合的組成部分可以用下面這張圖概括: ![1595682050240-6c6946f2-9dd4-4e5b-a006-39144184e2f1.png](https://cdn.nlark.com/yuque/0/2020/png/1694029/1596107343106-b98a8b29-06ba-4f6f-ab14-7a139e506dd5.png) 在 Set 集合體系中,我們需要著重關注兩點: - 存入**可變元素**時,必須非常小心,因為任意時候元素狀態的改變都有可能使得 Set 內部出現兩個**相等**的元素,即 `o1.equals(o2) = true`,所以一般不要更改存入 Set 中的元素,否則將會破壞了 `equals()` 的作用! - Set 的最大作用就是判重,在專案中最大的作用也是**判重**! 接下來我們去看它的實現類和子類: `AbstractSet` 和 `SortedSet` ### AbstractSet 抽象類 `AbstractSet` 是一個實現 Set 的一個抽象類,定義在這裡可以將所有具體 Set 集合的**相同行為**在這裡實現,**避免子類包含大量的重複程式碼** 所有的 Set 也應該要有相同的 `hashCode()` 和 `equals()` 方法,所以使用抽象類把該方法重寫後,子類無需關心這兩個方法。 ```Java public abstract class AbstractSet implements Set { // 判斷兩個 set 是否相等 public boolean equals(Object o) { if (o == this) { // 集合本身 return true; } else if (!(o instanceof Set)) { // 集合不是 set return false; } else { // 比較兩個集合的元素是否全部相同 } } // 計算所有元素的 hashcode 總和 public int hashCode() { int h = 0; Iterator i = this.iterator(); while(i.hasNext()) { E obj = i.next(); if (obj != null) { h += obj.hashCode(); } } return h; } } ``` ### SortedSet 介面 `SortedSet` 是一個介面,它在 Set 的基礎上擴充套件了**排序**的行為,所以所有實現它的子類都會擁有排序功能。 ```Java public interface SortedSet extends Set { // 元素的比較器,決定元素的排列順序 Comparator comparator(); // 獲取 [var1, var2] 之間的 set SortedSet subSet(E var1, E var2); // 獲取以 var1 開頭的 Set SortedSet headSet(E var1); // 獲取以 var1 結尾的 Set SortedSet tailSet(E var1); // 獲取首個元素 E first(); // 獲取最後一個元素 E last(); } ``` ### HashSet HashSet 底層藉助 `HashMap` 實現,我們可以觀察它的多個構造方法,本質上都是 new 一個 HashMap > 這也是這篇文章為什麼先講解 Map 再講解 Set 的原因!先學習 Map,有助於理解 Set ```Java public class HashSet extends AbstractSet implements Set, Cloneable, Serializable { public HashSet() { this.map = new HashMap(); } public HashSet(int initialCapacity, float loadFactor) { this.map = new HashMap(initialCapacity, loadFactor); } public HashSet(int initialCapacity) { this.map = new HashMap(initialCapacity); } } ``` 我們可以觀察 `add()` 方法和`remove()`方法是如何將 HashSet 的操作嫁接到 HashMap 的。 ```Java private static final Object PRESENT = new Object(); public boolean add(E e) { return this.map.put(e, PRESENT) == null; } public boolean remove(Object o) { return this.map.remove(o) == PRESENT; } ``` 我們看到 `PRESENT` 就是一個**靜態常量**:使用 PRESENT 作為 HashMap 的 value 值,使用HashSet的開發者只需**關注**於需要插入的 `key`,**遮蔽**了 HashMap 的 `value` ![image.png](https://cdn.nlark.com/yuque/0/2020/png/1694029/1596109695209-9cd36cf2-1b1f-44db-94ee-71d60348d257.png) 上圖可以觀察到每個`Entry`的`value`都是 PRESENT 空物件,我們就不用再理會它了。 HashSet 在 HashMap 基礎上實現,所以很多地方可以聯絡到 HashMap: - 底層資料結構:HashSet 也是採用`陣列 + 連結串列 + 紅黑樹`實現 - 執行緒安全性:由於採用 HashMap 實現,而 HashMap 本身執行緒不安全,在HashSet 中沒有新增額外的同步策略,所以 HashSet 也**執行緒不安全** - 存入 HashSet 的物件的狀態**最好不要發生變化**,因為有可能改變狀態後,在集合內部出現兩個元素`o1.equals(o2)`,破壞了 `equals()`的語義。 ### LinkedHashSet LinkedHashSet 的程式碼少的可憐,不信我給你我粘出來 ![image.png](https://cdn.nlark.com/yuque/0/2020/png/1694029/1596108598046-702b165f-6aa0-463e-b87b-de9305acb693.png) 少歸少,還是不能鬧,`LinkedHashSet`繼承了`HashSet`,我們跟隨到父類 HashSet 的構造方法看看 ```java HashSet(int initialCapacity, float loadFactor, boolean dummy) { this.map = new LinkedHashMap(initialCapacity, loadFactor); } ``` 發現父類中 map 的實現採用`LinkedHashMap`,這裡注意不是`HashMap`,而 LinkedHashMap 底層又採用 HashMap + 雙向連結串列 實現的,所以本質上 LinkedHashSet 還是使用 HashMap 實現的。 > LinkedHashSet -> LinkedHashMap -> HashMap + 雙向連結串列 ![image.png](https://cdn.nlark.com/yuque/0/2020/png/1694029/1596110006108-d064aa74-d71c-401a-94b9-83caef7cdbc7.png) 而 LinkedHashMap 是採用 `HashMap`和`雙向連結串列`實現的,這條雙向連結串列中儲存了元素的插入順序。所以 LinkedHashSet 可以按照元素的插入順序遍歷元素,如果你熟悉`LinkedHashMap`,那 LinkedHashSet 也就更不在話下了。 關於 LinkedHashSet 需要注意幾個地方: - 它繼承了 `HashSet`,而 HashSet 預設是採用 HashMap 儲存資料的,但是 LinkedHashSet 呼叫父類構造方法初始化 map 時是 LinkedHashMap 而不是 HashMap,這個要額外注意一下 - 由於 LinkedHashMap 不是執行緒安全的,且在 LinkedHashSet 中沒有新增額外的同步策略,所以 LinkedHashSet 集合**也不是執行緒安全**的 ### TreeSet TreeSet 是基於 TreeMap 的實現,所以儲存的元素是**有序**的,底層的資料結構是`陣列 + 紅黑樹`。 ![img](https://cdn.nlark.com/yuque/0/2020/png/1694029/1596126807774-1a122d9e-4210-4708-8328-c23f65bc55dd.png) 而元素的排列順序有`2`種,和 TreeMap 相同:自然排序和定製排序,常用的構造方法已經在下面展示出來了,TreeSet 預設按照自然排序,如果需要定製排序,需要傳入`Comparator`。 ```Java public TreeSet() { this(new TreeMap()); } public TreeSet(Comparator comparator) { this(new TreeMap<>(comparator)); } ``` TreeSet 應用場景有很多,像在遊戲裡的玩家戰鬥力排行榜 ```Java public class Player implements Comparable { public String name; public int score; @Override public int compareTo(Student o) { return Integer.compareTo(this.score, o.score); } } public static void main(String[] args) { Player s1 = new Player("張三", 100); Player s2 = new Player("李四", 90); Player s3 = new Player("王五", 80); TreeSet set = new TreeSet(); set.add(s2); set.add(s1); set.add(s3); System.out.println(set); } // [Student{name='王五', score=80}, Student{name='李四', score=90}, Student{name='張三', score=100}] ``` 對 TreeSet 介紹了它的主要實現方式和應用場景,有幾個值得注意的點。 - TreeSet 的所有操作都會轉換為對 TreeMap 的操作,TreeMap 採用**紅黑樹**實現,任意操作的平均時間複雜度為 `O(logN)` - TreeSet 是一個**執行緒不安全**的集合 - TreeSet 常應用於對**不重複**的元素**定製排序**,例如玩家戰力排行榜 > 注意:TreeSet判斷元素是否重複的方法是判斷compareTo()方法是否返回0,而不是呼叫 hashcode() 和 equals() 方法,如果返回 0 則認為集合內已經存在相同的元素,不會再加入到集合當中。 ## List 介面 List 介面和 Set 介面齊頭並進,是我們日常開發中接觸的很多的一種集合型別了。整個 List 集合的組成部分如下圖
`List` 介面直接繼承 Collection 介面,它定義為可以儲存**重複**元素的集合,並且元素按照插入順序**有序排列**,且可以通過**索引**訪問指定位置的元素。常見的實現有:ArrayList、LinkedList、Vector和Stack ### AbstractList 和 AbstractSequentialList AbstractList 抽象類實現了 List 介面,其內部實現了所有的 List 都需具備的功能,子類可以專注於實現自己具體的操作邏輯。 ```java // 查詢元素 o 第一次出現的索引位置 public int indexOf(Object o) // 查詢元素 o 最後一次出現的索引位置 public int lastIndexOf(Object o) //··· ``` AbstractSequentialList 抽象類繼承了 AbstractList,在原基礎上限制了訪問元素的順序**只能夠按照順序訪問**,而**不支援隨機訪問**,如果需要滿足隨機訪問的特性,則繼承 AbstractList。子類 LinkedList 使用連結串列實現,所以僅能支援**順序訪問**,顧繼承了 `AbstractSequentialList`而不是 AbstractList。 ### Vector ![image.png](https://cdn.nlark.com/yuque/0/2020/png/1694029/1595375364068-c2c49168-a2f4-4a06-97c0-eb3446d60a68.png) `Vector` 在現在已經是一種過時的集合了,包括繼承它的 `Stack` 集合也如此,它們被淘汰的原因都是因為**效能**低下。 >
JDK 1.0 時代,ArrayList 還沒誕生,大家都是使用 Vector 集合,但由於 Vector 的**每個操作**都被 **synchronized** 關鍵字修飾,即使線上程安全的情況下,仍然**進行無意義的加鎖與釋放鎖**,造成額外的效能開銷,做了無用功。 ```java public synchronized boolean add(E e); public synchronized E get(int index); ``` 在 JDK 1.2 時,Collection 家族出現了,它提供了大量**高效能、適用於不同場合**的集合,而 Vector 也是其中一員,但由於 Vector 在每個方法上都加了鎖,由於需要相容許多老的專案,很難在此基礎上優化`Vector`了,所以漸漸地也就被歷史淘汰了。 現在,在**執行緒安全**的情況下,不需要選用 Vector 集合,取而代之的是 **ArrayList** 集合;在併發環境下,出現了 `CopyOnWriteArrayList`,Vector 完全被棄用了。 ### Stack ![img](https://cdn.nlark.com/yuque/0/2020/png/1694029/1596126551356-dc1af780-2fe9-4d04-8351-e70637ecdab5.png) `Stack`是一種`後入先出(LIFO)`型的集合容器,如圖中所示,`大雄`是最後一個進入容器的,top指標指向大雄,那麼彈出元素時,大雄也是第一個被彈出去的。 Stack 繼承了 Vector 類,提供了棧頂的壓入元素操作(push)和彈出元素操作(pop),以及檢視棧頂元素的方法(peek)等等,但由於繼承了 Vector,正所謂跟錯老大沒福報,Stack 也漸漸被淘汰了。 取而代之的是後起之秀 `Deque`介面,其實現有 `ArrayDeque`,該資料結構更加完善、可靠性更好,依靠佇列也可以實現`LIFO`的棧操作,所以優先選擇 ArrayDeque 實現棧。 ```java Deque stack = new ArrayDeque(); ``` ArrayDeque 的資料結構是:`陣列`,並提供**頭尾指標下標**對陣列元素進行操作。本文也會講到哦,客官請繼續往下看,莫著急!:smile: ### ArrayList ArrayList 以**陣列**作為儲存結構,它是**執行緒不安全**的集合;具有**查詢快、在陣列中間或頭部增刪慢**的特點,所以它除了執行緒不安全這一點,其餘可以替代`Vector`,而且執行緒安全的 ArrayList 可以使用 `CopyOnWriteArrayList`代替 Vector。 ![image.png](https://cdn.nlark.com/yuque/0/2020/png/1694029/1595375364068-c2c49168-a2f4-4a06-97c0-eb3446d60a68.png) 關於 ArrayList 有幾個重要的點需要注意的: - 具備**隨機訪問**特點,**訪問元素的效率**較高,ArrayList 在**頻繁插入、刪除**集合元素的場景下效率較`低`。 - 底層資料結構:ArrayList 底層使用陣列作為儲存結構,具備**查詢快、增刪慢**的特點 - 執行緒安全性:ArrayList 是**執行緒不安全**的集合 - ArrayList **首次擴容**後的長度為 `10`,呼叫 `add()` 時需要計算容器的最小容量。可以看到如果陣列`elementData`為空陣列,會將最小容量設定為`10`,之後會將陣列長度完成首次擴容到 10。 ```Java // new ArrayList 時的預設空陣列 private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}; // 預設容量 private static final int DEFAULT_CAPACITY = 10; // 計算該容器應該滿足的最小容量 private static int calculateCapacity(Object[] elementData, int minCapacity) { if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { return Math.max(DEFAULT_CAPACITY, minCapacity); } return minCapacity; } ``` - 集合從**第二次擴容**開始,陣列長度將擴容為原來的 `1.5` 倍,即:`newLength = oldLength * 1.5` ![image.png](https://cdn.nlark.com/yuque/0/2020/png/1694029/1595724934991-c2f12445-bc9e-4e1c-b965-04f2ef9d3d79.png) ### LinkedList LinkedList 底層採用`雙向連結串列`資料結構儲存元素,由於連結串列的記憶體地址`非連續`,所以它不具備隨機訪問的特點,但由於它利用指標連線各個元素,所以插入、刪除元素只需要`操作指標`,不需要`移動元素`,故具有**增刪快、查詢慢**的特點。它也是一個非執行緒安全的集合。 ![image.png](https://cdn.nlark.com/yuque/0/2020/png/1694029/1595393003456-d37281f4-8332-46b4-9d81-e1f0c24dc060.png) 由於以雙向連結串列作為資料結構,它是**執行緒不安全**的集合;儲存的每個節點稱為一個`Node`,下圖可以看到 Node 中儲存了`next`和`prev`指標,`item`是該節點的值。在插入和刪除時,時間複雜度都保持為 `O(1)` ![image.png](https://cdn.nlark.com/yuque/0/2020/png/1694029/1595725358023-1f64f780-9dd0-47ff-a84c-d4101d16c1e1.png) 關於 LinkedList,除了它是以連結串列實現的集合外,還有一些特殊的特性需要注意的。 - 優勢:LinkedList 底層沒有`擴容機制`,使用`雙向連結串列`儲存元素,所以插入和刪除元素效率較高,適用於頻繁操作元素的場景 - 劣勢:LinkedList 不具備`隨機訪問`的特點,查詢某個元素只能從 `head` 或 `tail` 指標一個一個比較,所以**查詢中間的元素時效率很低** - 查詢優化:LinkedList 查詢某個下標 `index` 的元素時**做了優化**,若 `index > (size / 2)`,則從 `head` 往後查詢,否則從 `tail` 開始往前查詢,程式碼如下所示: ```Java LinkedList.Node node(int index) { LinkedList.Node x; int i; if (index < this.size >> 1) { // 查詢的下標處於連結串列前半部分則從頭找 x = this.first; for(i = 0; i < index; ++i) { x = x.next; } return x; } else { // 查詢的下標處於陣列的後半部分則從尾開始找 x = this.last; for(i = this.size - 1; i > index; --i) { x = x.prev; } return x; } } ``` - 雙端佇列:使用雙端連結串列實現,並且實現了 `Deque` 介面,使得 LinkedList 可以用作**雙端佇列**。下圖可以看到 Node 是集合中的元素,提供了前驅指標和後繼指標,還提供了一系列操作`頭結點`和`尾結點`的方法,具有雙端佇列的特性。 ![image.png](https://cdn.nlark.com/yuque/0/2020/png/1694029/1595693779116-a8156f03-36fa-4557-892e-ea5103b06136.png) LinkedList 集合最讓人樹枝的是它的連結串列結構,但是我們同時也要注意它是一個雙端佇列型的集合。 ```java Deque deque = new LinkedList<>(); ``` ## Queue介面 `Queue`佇列,在 JDK 中有兩種不同型別的集合實現:**單向佇列**(AbstractQueue) 和 **雙端佇列**(Deque) ![img](https://cdn.nlark.com/yuque/0/2020/png/1694029/1595684241064-e863aeca-6a95-4423-92c4-762f56be1dbe.png) Queue 中提供了兩套增加、刪除元素的 API,當插入或刪除元素失敗時,會有**兩種不同的失敗處理策略**。 | 方法及失敗策略 | 插入方法 | 刪除方法 | 查詢方法 | | :------------- | :------- | :------- | -------- | | 丟擲異常 | add() | remove() | get() | | 返回失敗預設值 | offer() | poll() | peek() | 選取哪種方法的決定因素:插入和刪除元素失敗時,希望`丟擲異常`還是返回`布林值` `add()` 和 `offer()` 對比: 在佇列長度大小確定的場景下,佇列放滿元素後,新增下一個元素時,add() 會丟擲 `IllegalStateException`異常,而 `offer()` 會返回 `false` 。 但是它們兩個方法在插入**某些不合法的元素**時都會丟擲三個相同的異常。 ![image.png](https://cdn.nlark.com/yuque/0/2020/png/1694029/1595691512036-ed9fd3ea-5432-4105-a3fb-a5374d571971.png) `remove()` 和 `poll()` 對比: 在**佇列為空**的場景下, `remove()` 會丟擲 `NoSuchElementException`異常,而 `poll()` 則返回 `null` 。 `get()`和`peek()`對比: 在佇列為空的情況下,`get()`會丟擲`NoSuchElementException`異常,而`peek()`則返回`null`。 ### Deque 介面 `Deque` 介面的實現非常好理解:從**單向**佇列演變為**雙向**佇列,內部額外提供**雙向佇列的操作方法**即可: ![image.png](https://cdn.nlark.com/yuque/0/2020/png/1694029/1596166722772-975ff644-6abf-441b-b678-4a6de5b0eef1.png) Deque 介面額外提供了**針對佇列的頭結點和尾結點**操作的方法,而**插入、刪除方法同樣也提供了兩套不同的失敗策略**。除了`add()`和`offer()`,`remove()`和`poll()`以外,還有`get()`和`peek()`出現了不同的策略 ### AbstractQueue 抽象類 AbstractQueue 類中提供了各個 API 的基本實現,主要針對各個不同的處理策略給出基本的方法實現,定義在這裡的作用是讓`子類`根據其`方法規範`(操作失敗時丟擲異常還是返回預設值)實現具體的業務邏輯。 ![image.png](https://cdn.nlark.com/yuque/0/2020/png/1694029/1596167156067-36121579-8127-4019-ba47-e4de73f05cda.png) ### LinkedList LinkedList 在上面已經詳細解釋了,它實現了 `Deque` 介面,提供了針對頭結點和尾結點的操作,並且每個結點都有**前驅**和**後繼**指標,具備了雙向佇列的所有特性。 ### ArrayDeque 使用**陣列**實現的雙端佇列,它是**無界**的雙端佇列,最小的容量是`8`(JDK 1.8)。在 JDK 11 看到它預設容量已經是 `16`了。 ![image.png](https://cdn.nlark.com/yuque/0/2020/png/1694029/1595695213834-cb4f1c3a-e07a-42aa-981f-31a896febe26.png) `ArrayDeque` 在日常使用得不多,值得注意的是它與 `LinkedList` 的對比:`LinkedList` 採用**連結串列**實現雙端佇列,而 `ArrayDeque` 使用**陣列**實現雙端佇列。 > 在文件中作者寫到:**ArrayDeque 作為棧時比 Stack 效能好,作為佇列時比 LinkedList 效能好** 由於雙端佇列**只能在頭部和尾部**操作元素,所以刪除元素和插入元素的時間複雜度大部分都穩定在 `O(1)` ,除非在擴容時會涉及到元素的批量複製操作。但是在大多數情況下,使用它時應該指定一個大概的陣列長度,避免頻繁的擴容。 > 個人觀點:連結串列的插入、刪除操作涉及到**指標的操作,我個人認為作者是覺得陣列下標的移動要比指標的操作要廉價,而且陣列**採用**連續**的記憶體地址空間,而**連結串列**元素的記憶體地址是**不連續**的,所以陣列操作元素的效率在**定址上**會比連結串列要快。請批判看待觀點。 ### PriorityQueue PriorityQueue 基於**優先順序堆實現**的優先順序佇列,而堆是採用**陣列**實現: ![image.png](https://cdn.nlark.com/yuque/0/2020/png/1694029/1595727271522-d144468c-041e-4721-a786-9f952f06fafe.png) 文件中的描述告訴我們:該陣列中的元素通過傳入 `Comparator` 進行定製排序,如果不傳入`Comparator`時,則按照元素本身`自然排序`,但要求元素實現了`Comparable`介面,所以 PriorityQueue **不允許儲存 NULL 元素**。 PriorityQueue 應用場景:元素本身具有優先順序,需要按照**優先順序處理元素** - 例如遊戲中的VIP玩家與普通玩家,VIP 等級越高的玩家越先安排進入伺服器玩耍,減少玩家流失。 ```Java public static void main(String[] args) { Student vip1 = new Student("張三", 1); Student vip3 = new Student("洪七", 2); Student vip4 = new Student("老八", 4); Student vip2 = new Student("李四", 1); Student normal1 = new Student("王五", 0); Student normal2 = new Student("趙六", 0); // 根據玩家的 VIP 等級進行降序排序 PriorityQueue queue = new PriorityQueue<>((o1, o2) -> o2.getScore().compareTo(o1.getScore())); queue.add(vip1);queue.add(vip4);queue.add(vip3); queue.add(normal1);queue.add(normal2);queue.add(vip2); while (!queue.isEmpty()) { Student s1 = queue.poll(); System.out.println(s1.getName() + "進入遊戲; " + "VIP等級: " + s1.getScore()); } } public static class Student implements Comparable { private String name; private Integer score; public Student(String name, Integer score) { this.name = name; this.score = score; } @Override public int compareTo(Student o) { return this.score.compareTo(o.getScore()); } } ``` 執行上面的程式碼可以得到下面這種有趣的結果,可以看到`氪金`使人帶來快樂。 ![image.png](https://cdn.nlark.com/yuque/0/2020/png/1694029/1595727945968-768b45bb-96dc-4850-8759-f07776107a23.png) VIP 等級越高(優先順序越高)就越優先安排進入遊戲(優先處理),類似這種有優先順序的場景還有非常多,各位可以發揮自己的想象力。 PriorityQueue 總結: - PriorityQueue 是基於**優先順序堆**實現的優先順序佇列,而堆是用**陣列**維護的 - PriorityQueue 適用於**元素按優先順序處理**的業務場景,例如使用者在請求人工客服需要排隊時,根據使用者的**VIP等級**進行 `插隊` 處理,等級越高,越先安排客服。 章節結束各集合總結:(以 JDK1.8 為例) | 資料型別 | 插入、刪除時間複雜度 | 查詢時間複雜度 | 底層資料結構 | 是否執行緒安全 | | :------------ | :------------------- | :------------- | :------------------- | :----------- | | Vector | O(N) | O(1) | 陣列 | 是(已淘汰) | | ArrayList | O(N) | O(1) | 陣列 | 否 | | LinkedList | O(1) | O(N) | 雙向連結串列 | 否 | | HashSet | O(1) | O(1) | 陣列+連結串列+紅黑樹 | 否 | | TreeSet | O(logN) | O(logN) | 紅黑樹 | 否 | | LinkedHashSet | O(1) | O(1)~O(N) | 陣列 + 連結串列 + 紅黑樹 | 否 | | ArrayDeque | O(N) | O(1) | 陣列 | 否 | | PriorityQueue | O(logN) | O(logN) | 堆(陣列實現) | 否 | | HashMap | O(1) ~ O(N) | O(1) ~ O(N) | 陣列+連結串列+紅黑樹 | 否 | | TreeMap | O(logN) | O(logN) | 陣列+紅黑樹 | 否 | | HashTable | O(1) / O(N) | O(1) / O(N) | 陣列+連結串列 | 是(已淘汰) | ## 文末總結 這一篇文章對各個集合都有些`點到即止`的味道,此文的目的是對整個集合框架有一個較為整體的瞭解,分析了最常用的集合的相關特性,以及某些特殊集合的應用場景例如`TreeSet`、`TreeMap`這種可定製排序的集合。 - `Collection` 介面提供了整個集合框架**最通用**的增刪改查以及集合自身操作的抽象方法,讓子類去實現 - `Set` 介面決定了它的子類都是**無序、無重複元素**的集合,其主要實現有HashSet、TreeSet、LinkedHashSet。 - `HashSet` 底層採用 `HashMap` 實現,而 `TreeSet` 底層使用 `TreeMap` 實現,大部分 Set 集合的操作都會轉換為 Map 的操作,TreeSet 可以將元素按照規則進行**排序**。 - `List` 介面決定了它的子類都是**有序、可儲存重複元素**的集合,常見的實現有 ArrayList,LinkedList,Vector - `ArrayList` 使用**陣列**實現,而 LinkedList 使用**連結串列**實現,所以它們兩個的使用場景幾乎是相反的,**頻繁查詢**的場景使用 ArrayList,而**頻繁插入刪除**的場景最好使用 LinkedList - `LinkedList` 和 `ArrayDeque` 都可用於**雙端佇列**,而 *Josh Bloch and Doug Lea* 認為 `ArrayDeque` 具有比 `LinkedList` 更好的效能,`ArrayDeque`使用**陣列**實現雙端佇列,`LinkedList`使用**連結串列**實現雙端佇列。 - `Queue` 介面定義了佇列的基本操作,子類集合都會擁有佇列的特性:**先進先出**,主要實現有:LinkedList,ArrayDeque - `PriorityQueue` 底層使用**二叉堆**維護的優先順序佇列,而二叉堆是由**陣列**實現的,它可以按照元素的優先順序進行排序,**優先順序越高的元素,排在佇列前面,優先被彈出處理**。 - `Map`介面定義了該種集合型別是以``鍵值對形式儲存,其主要實現有:HashMap,TreeMap,LinkedHashMap,Hashtable - LinkedHashMap 底層多加了一條雙向連結串列,設定`accessOrder`為`true`並重寫方法則可以實現`LRU`快取 - TreeMap 底層採用陣列+紅黑樹實現,集合內的元素預設按照自然排序,也可以傳入`Comparator`定製排序 看到這裡非常不容易,感謝你願意閱讀我的文章,希望能對你有所幫助,你可以參考著文末總結的順序,每當我提到一個集合時,回想它的重要知識點是什麼,主要就是`底層資料結構`,`執行緒安全性`,`該集合的一兩個特有性質`,只要能夠回答出來個大概,我相信之後運用這些資料結構,你能夠熟能生巧。 本文對整個集合體系的所有常用的集合類都分析了,這裡並沒有對集合內部的實現深入剖析,我想先從最巨集觀的角度讓大家瞭解每個集合的的作用,應用場景,以及簡單的對比,之後會抽時間對常見的集合進行原始碼剖析,盡情期待,感謝閱讀! > 最後有些話想說:這篇文章花了我半個月去寫,也是意義重大,多謝 `cxuan`哥一直指導我寫文章,一步一步地去打磨出一篇好的文章真的非常不容易,寫下的每一個字都能夠讓別人看得懂是一件非常難的事情,總結出最精華的知識分享給你們也是非常難的一件事情,希望能夠一直進步下去!不忘初心,熱愛分享,喜愛寫作。 ![](https://img2020.cnblogs.com/blog/1515111/202008/1515111-20200803081850221-1069203927.png) ![](https://img2020.cnblogs.com/blog/1515111/202008/1515111-20200803081839915-7318051