Java 集合框架體系總覽

飛天小牛肉發表於2021-03-11

? 盡人事,聽天命。博主東南大學碩士在讀,熱愛健身和籃球,樂於分享技術相關的所見所得,關注公眾號 @ 飛天小牛肉,第一時間獲取文章更新,成長的路上我們一起進步

? 本文已收錄於 「CS-Wiki」Gitee 官方推薦專案,現已累計 1.5k+ star,致力打造完善的後端知識體系,在技術的路上少走彎路,歡迎各位小夥伴前來交流學習

? 如果各位小夥伴春招秋招沒有拿得出手的專案的話,可以參考我寫的一個專案「開源社群系統 Echo」Gitee 官方推薦專案,目前已累計 500+ star,基於 SpringBoot + MyBatis + MySQL + Redis + Kafka + Elasticsearch + Spring Security + ... 並提供詳細的開發文件和配套教程。公眾號後臺回覆 Echo 可以獲取配套教程,目前尚在更新中


集合這塊知識的重要性不用多說,加上多執行緒妥妥的穩佔面試必問霸主地主,深入瞭解集合框架的整體結構以及各個集合類的實現原理是非常有必要的。

由於不同的集合在實現上採用了各種不同的資料結構,導致了各個集合的效能、底層實現、使用方式上存在一定的差異,所以集合這塊的知識點非常多,不過好在它的整體學習框架比較清晰。本文只籠統介紹集合框架的知識體系,幫助大家理清思路,重點集合類的詳細分析之後會單獨分成幾篇文章。

全文脈絡思維導圖如下:

1. 為什麼要使用集合

當我們在學習一個東西的時候,最好是明白為什麼要使用這個東西,不要為了用而用,知其然而知其所以然。

集合,故名思議,是用來儲存元素的,而陣列也同樣具有這個功能,那麼既然出現了集合,必然是因為陣列的使用存在一定的缺陷

上篇文章已經簡單提到過,陣列一旦被定義,就無法再更改其儲存大小。舉個例子,假設有一個班級,現在有 50 個學生在這個班裡,於是我們定義了一個能夠儲存 50 個學生資訊的陣列:

1)如果這個班裡面來了 10 個轉班生,由於陣列的長度固定不變,那麼顯然這個陣列的儲存能力無法支援 60 個學生;再比如,這個班裡面有 20 個學生退學了,那麼這個陣列實際上只存了 30 個學生,造成了記憶體空間浪費。總結來說,由於陣列一旦被定義,就無法更改其長度,所以陣列無法動態的適應元素數量的變化

2)陣列擁有 length 屬性,可以通過這個屬性查到陣列的儲存能力也就是陣列的長度,但是無法通過一個屬性直接獲取到陣列中實際儲存的元素數量。

3)因為陣列在記憶體中採用連續空間分配的儲存方式,所以我們可以根據下標快速獲的取對應的學生資訊。比如我們在陣列下標為 2 的位置存入了某個學生的學號 111,那顯然,直接通過下標 2 就能獲取學號 111。但是如果反過來我們想要查詢學號 111 的下標呢?陣列原生是做不到的,這就需要使用各種查詢演算法了。

4)另外,假如我們想要儲存學生的姓名和家庭地址的一一對應資訊,陣列顯然也是做不到的。

5)如果我們想在這個用來儲存學生資訊的陣列中儲存一些老師的資訊,陣列是無法滿足這個需求的,它只能儲存相同型別的元素。

為了解決這些陣列在使用過程中的痛點,集合框架應用而生。簡單來說,集合的主要功能就是兩點:

  • 儲存不確定數量的資料(可以動態改變集合長度)
  • 儲存具有對映關係的資料
  • 儲存不同型別的資料

不過,需要注意的是,集合只能儲存引用型別(物件),如果你儲存的是 int 型資料(基本型別),它會被自動裝箱成 Integer 型別。而陣列既可以儲存基本資料型別,也可以儲存引用型別

2. 集合框架體系速覽

與現代的資料結構類庫的常見情況一樣,Java 集合類也將介面與實現分離,這些介面和實現類都位於 java.util 包下。按照其儲存結構集合可以分為兩大類:

  • 單列集合 Collection
  • 雙列集合 Map

Collection 介面

單列集合 java.util.Collection:元素是孤立存在的,向集合中儲存元素採用一個個元素的方式儲存。

來看 Collection 介面的繼承體系圖:

Collection 介面中定義了一些單列集合通用的方法:

public boolean add(E e); // 把給定的物件新增到當前集合中
public void clear(); // 清空集合中所有的元素
public boolean remove(E e); // 把給定的物件在當前集合中刪除
public boolean contains(E e); // 判斷當前集合中是否包含給定的物件
public boolean isEmpty(); // 判斷當前集合是否為空
public int size(); // 返回集合中元素的個數
public Object[] toArray(); // 把集合中的元素,儲存到陣列中

Collection 有兩個重要的子介面,分別是 ListSet,它們分別代表了有序集合和無序集合:

1)List 的特點是元素有序、可重複,這裡所謂的有序意思是:元素的存入順序和取出順序一致。例如,儲存元素的順序是 11、22、33,那麼我們從 List 中取出這些元素的時候也會按照 11、22、33 這個順序。List 介面的常用實現類有:

  • ArrayList:底層資料結構是陣列,執行緒不安全
  • LinkedList:底層資料結構是連結串列,執行緒不安全

除了包括 Collection 介面的所有方法外,List 介面而且還增加了一些根據元素索引來操作集合的特有方法:

public void add(int index, E element); // 將指定的元素,新增到該集合中的指定位置上
public E get(int index); // 返回集合中指定位置的元素
public E remove(int index); // 移除列表中指定位置的元素, 返回的是被移除的元素
public E set(int index, E element); // 用指定元素替換集合中指定位置的元素

2)Set 介面在方法簽名上與 Collection 介面其實是完全一樣的,只不過在方法的說明上有更嚴格的定義,最重要的特點是他拒絕新增重複元素,不能通過整數索引來訪問,並且元素無序。所謂無序也就是元素的存入順序和取出順序不一致。其常用實現類有:

  • HashSet:底層基於 HashMap 實現,採用 HashMap 來儲存元素
  • LinkedHashSetLinkedHashSetHashSet 的子類,並且其底層是通過 LinkedHashMap 來實現的。

至於為什麼要定義一個方法簽名完全相同的介面,我的理解是為了讓集合框架的結構更加清晰,將單列集合從以下兩點區分開來:

  • 可以新增重複元素(List)和不可以新增重複元素(Set)
  • 可以通過整數索引訪問(List)和不可以通過整數索引(Set)

這樣當我們宣告單列集合時能夠更準確的繼承相應的介面。

Map 介面

雙列集合 java.util.Map:元素是成對存在的。每個元素由鍵(key)與值(value)兩部分組成,通過鍵可以找對所對應的值。顯然這個雙列集合解決了陣列無法儲存對映關係的痛點。另外,需要注意的是,Map 不能包含重複的鍵,值可以重複;並且每個鍵只能對應一個值

來看 Map 介面的繼承體系圖:

Map 介面中定義了一些雙列集合通用的方法:

public V put(K key, V value); // 把指定的鍵與指定的值新增到 Map 集合中。
public V remove(Object key); // 把指定的鍵所對應的鍵值對元素在 Map 集合中刪除,返回被刪除元素的值。
public V get(Object key); // 根據指定的鍵,在 Map 集合中獲取對應的值。
boolean containsKey(Object key); // 判斷集合中是否包含指定的鍵。
public Set<K> keySet(); // 獲取 Map 集合中所有的鍵,儲存到 Set 集合中。

Map 有兩個重要的實現類,HashMapLinkedHashMap

HashMap:可以說 HashMap 不背到滾瓜爛熟不敢去面試,這裡簡單說下它的底層結構,後面會開文詳細講解。JDK 1.8 之前 HashMap 底層由陣列加連結串列實現,陣列是 HashMap 的主體,連結串列則是主要為了解決雜湊衝突而存在的(“拉鍊法” 解決衝突)。JDK1.8 以後在解決雜湊衝突時有了較大的變化,當連結串列長度大於閾值(預設為 8)時,將連結串列轉化為紅黑樹,以減少搜尋時間(注意:將連結串列轉換成紅黑樹前會判斷,如果當前陣列的長度小於 64,那麼會選擇先進行陣列擴容,而不是轉換為紅黑樹)。

LinkedHashMapHashMap 的子類,可以保證元素的存取順序一致(存進去時候的順序是多少,取出來的順序就是多少,不會因為 key 的大小而改變)。

LinkedHashMap 繼承自 HashMap,所以它的底層仍然是基於拉鍊式雜湊結構,即由陣列和連結串列或紅黑樹組成。另外,LinkedHashMap 在上面結構的基礎上,增加了一條雙向連結串列,使得上面的結構可以保持鍵值對的插入順序。同時通過對連結串列進行相應的操作,實現了訪問順序相關邏輯。

OK,我們已經知道,Map中存放的是兩種物件,一種稱為 key(鍵),一種稱為 value(值),它倆在 Map 中是一一對應關係,這一對物件又稱做 Map 中的一個 Entry(項)。Entry 將鍵值對的對應關係封裝成了物件,即鍵值對物件。 Map 中也提供了獲取所有 Entry 物件的方法:

public Set<Map.Entry<K,V>> entrySet(); // 獲取 Map 中所有的 Entry 物件的集合。

同樣的,Map 也提供了獲取每一個 Entry 物件中對應鍵和對應值的方法,這樣我們在遍歷 Map 集合時,就可以從每一個鍵值對(Entry)物件中獲取對應的鍵與對應的值了:

public K getKey(); // 獲取某個 Entry 物件中的鍵。
public V getValue(); // 獲取某個 Entry 物件中的值。

下面我們結合上述所學,來看看 Map 的兩種遍歷方式:

1)遍歷方式一:根據 key 找值方式

  • 獲取 Map 中所有的鍵,由於鍵是唯一的,所以返回一個 Set 集合儲存所有的鍵。方法提示:keyset()

  • 遍歷鍵的 Set 集合,得到每一個鍵。

  • 根據鍵,獲取鍵所對應的值。方法提示:get(K key)

public static void main(String[] args) {
    // 建立 Map 集合物件 
    HashMap<Integer, String> map = new HashMap<Integer,String>();
    // 新增元素到集合 
    map.put(1, "小五");
    map.put(2, "小紅");
    map.put(3, "小張");

    // 獲取所有的鍵  獲取鍵集
    Set<Integer> keys = map.keySet();
    // 遍歷鍵集 得到 每一個鍵
    for (Integer key : keys) {
        // 獲取對應值
        String value = map.get(key);
        System.out.println(key + ":" + value);
    }  
}

這裡面不知道大家有沒有注意一個細節,keySet 方法的返回結果是 Set。Map 由於沒有實現 Iterable 介面,所以不能直接使用迭代器或者 for each 迴圈進行遍歷,但是轉成 Set 之後就可以使用了。至於迭代器是啥請繼續往下看。

2)遍歷方式二:鍵值對方式

  • 獲取 Map 集合中,所有的鍵值對 (Entry) 物件,以 Set 集合形式返回。方法提示:entrySet()

  • 遍歷包含鍵值對 (Entry) 物件的 Set 集合,得到每一個鍵值對 (Entry) 物件。

  • 獲取每個 Entry 物件中的鍵與值。方法提示:getkey()、getValue()

// 獲取所有的 entry 物件
Set<Entry<Integer,String>> entrySet = map.entrySet();

// 遍歷得到每一個 entry 物件
for (Entry<Integer, String> entry : entrySet) {
    Integer key = entry.getKey();
    String value = entry.getValue();  
    System.out.println(key + ":" + value);
}

3. 迭代器 Iterator

什麼是 Iterator

在上一章陣列中我們講過 for each 迴圈:

for(variable : collection) {
    // todo
}

collection 這一表示式必須是一個陣列或者是一個實現了 Iterable 介面的類物件。可以看到 Collection 這個介面就繼承了 Itreable 介面,所以所有實現了 Collection 介面的集合都可以使用 for each 迴圈。

我們點進 Iterable 中看一看:

它擁有一個 iterator 方法,返回型別是 Iterator,這又是啥,我們再點進去看看:

又是三個介面,不過無法再跟下去了,我們去 Collection 的實現類中看看,有沒有實現 Itreator 這個介面,隨便開啟一個,比如 ArrayList

從原始碼可知:Iterator 介面在 ArrayList 中是以內部類的方式實現的。並且,Iterator 實際上就是在遍歷集合。

所以總結來說:我們可以通過 Iterator 介面遍歷 Collection 的元素,這個介面的具體實現是在具體的子類中,以內部類的方式實現。

❓ 這裡提個問題,為什麼迭代器不封裝成一個類,而是做成一個介面?假設迭代器是一個類,這樣我們就可以建立該類的物件,呼叫該類的方法來實現 Collection 的遍歷。

但事實上,Collection 介面有很多不同的實現類,在文章開頭我們就說過,這些類的底層資料結構大多是不一樣的,因此,它們各自的儲存方式和遍歷方式也是不同的,所以我們不能用一個類來規定死遍歷的方法。我們提取出遍歷所需要的通用方法,封裝進介面中,讓 Collection 的子類根據自己自身的特性分別去實現它。

看完上面這段分析,我們來驗證一下,看看 LinkedList 實現的 Itreator 介面和 ArrayList 實現的是不是不一樣:

顯然,這兩個雖然同為 Collection 的實現類,但是它們具體實現 Itreator 介面的內部過程是不一樣的。

Iterator 基本使用

OK,我們已經瞭解了 Iterator 是用來遍歷 Collection 集合的,那麼具體是怎麼遍歷的呢?

答:迭代遍歷

解釋一下迭代的概念:在取元素之前先判斷集合中有沒有元素,如果有,就把這個元素取出來,再繼續判斷,如果還有就再繼續取出來。一直到把集合中的所有元素全部取出。這種取出方式就稱為迭代。因此Iterator 物件也被稱為迭代器

也就是說,想要遍歷 Collection 集合,那麼就要獲取該集合對應的迭代器。如何獲取呢?其實上文已經出現過了,Collection 實現的 Iterable 中就有這樣的一個方法:iterator

再來介紹一下 Iterator 介面中的常用方法:

public E next(); // 返回迭代的下一個元素。
public boolean hasNext(); // 如果仍有元素可以迭代,則返回 true

舉個例子:

public static void main(String[] args) {
    Collection<String> coll = new ArrayList<String>();

    // 新增元素到集合
    coll.add("A");
    coll.add("B");
    coll.add("C");
    // 獲取 coll 的迭代器
    Iterator<String> it = coll.iterator();
    while(it.hasNext()){ // 判斷是否有迭代元素
        String s = it.next(); // 獲取迭代出的元素
        System.out.println(s);
    }
}

當然,用 for each 迴圈可以更加簡單地表示同樣的迴圈操作:

Collection<String> coll = new ArrayList<String>();
...
for(String element : coll){
    System.out.println(element);
}

References

? 關注公眾號 | 飛天小牛肉,即時獲取更新

  • 博主東南大學碩士在讀,利用課餘時間運營一個公眾號『 飛天小牛肉 』,2020/12/29 日開通,專注分享計算機基礎(資料結構 + 演算法 + 計算機網路 + 資料庫 + 作業系統 + Linux)、Java 基礎和麵試指南的相關原創技術好文。本公眾號的目的就是讓大家可以快速掌握重點知識,有的放矢。希望大家多多支援哦,和小牛肉一起成長 ?
  • 並推薦個人維護的開源教程類專案: CS-Wiki(Gitee 推薦專案,現已累計 1.5k+ star), 致力打造完善的後端知識體系,在技術的路上少走彎路,歡迎各位小夥伴前來交流學習 ~ ?
  • 如果各位小夥伴春招秋招沒有拿得出手的專案的話,可以參考我寫的一個專案「開源社群系統 Echo」Gitee 官方推薦專案,目前已累計 500+ star,基於 SpringBoot + MyBatis + MySQL + Redis + Kafka + Elasticsearch + Spring Security + ... 並提供詳細的開發文件和配套教程。公眾號後臺回覆 Echo 可以獲取配套教程,目前尚在更新中。

相關文章