計算機程式的思維邏輯 (55) – 容器類總結

swiftma發表於2019-03-02

本系列文章經補充和完善,已修訂整理成書《Java程式設計的邏輯》(馬俊昌著),由機械工業出版社華章分社出版,於2018年1月上市熱銷,讀者好評如潮!各大網店和書店有售,歡迎購買:京東自營連結

計算機程式的思維邏輯 (55) – 容器類總結

38節54節,我們介紹了多種容器類,本節進行簡要總結,我們主要從三個角度進行總結:

  • 用法和特點
  • 資料結構和演算法
  • 設計思維和模式

用法和特點

我們在52節展示過一張圖,其中包含了容器類主要的介面和類,我們還是用這個圖總結一下:

計算機程式的思維邏輯 (55) – 容器類總結

容器類有兩個根介面,分別是Collection和Map,Collection表示單個元素的集合,Map表示鍵值對的集合。

Collection表示的資料集合有基本的增、刪、查、遍歷等方法,但沒有定義元素間的順序或位置,也沒有規定是否有重複元素。

List是Collection的子介面,表示有順序或位置的資料集合,增加了根據索引位置進行操作的方法。它有兩個主要的實現類,ArrayListLinkedList,ArrayList基於陣列實現,LinkedList基於連結串列實現,ArrayList的隨機訪問效率很高,但從中間插入和刪除元素需要移動元素,效率比較低,LinkedList則正好相反,隨機訪問效率比較低,但增刪元素只需要調整鄰近節點的連結。

Set也是Collection的子介面,它沒有增加新的方法,但保證不含重複元素。它有兩個主要的實現類,HashSetTreeSet,HashSet基於雜湊表實現,要求鍵重寫hashCode方法,效率更高,但元素間沒有順序,TreeSet基於排序二叉樹實現,元素按比較有序,元素需要實現Comparable介面,或者建立TreeSet時提供一個Comparator物件。HashSet還有一個子類LinkedHashSet可以按插入有序。還有一個針對列舉型別的實現類EnumSet,它基於位向量實現,效率很高。

Queue是Collection的子介面,表示先進先出的佇列,在尾部新增,從頭部檢視或刪除。Deque是Queue的子介面,表示更為通用的雙端佇列,有明確的在頭或尾進行檢視、新增和刪除的方法。普通佇列有兩個主要的實現類,LinkedList和ArrayDeque,LinkedList基於連結串列實現,ArrayDeque基於迴圈陣列實現,一般而言,如果只需要Deque介面,ArrayDeque的效率更高一些。

Deque還有一個特殊的實現類PriorityQueue,它表示優先順序佇列,內部是用堆實現的,堆除了用於實現優先順序佇列,還可以高效方便的解決很多其他問題,比如求前K個最大的元素、求中值等。

Map介面表示鍵值對集合,經常根據鍵進行操作,它有兩個主要的實現類,HashMap和TreeMap。HashMap基於雜湊表實現,要求鍵重寫hashCode方法,操作效率很高,但元素沒有順序。TreeMap基於排序二叉樹實現,要求鍵實現Comparable介面,或提供一個Comparator物件,操作效率稍低,但可以按鍵有序。

HashMap還有一個子類LinkedHashMap,它可以按插入或訪問有序。之所以能有序,是因為每個元素還加入到了一個雙向連結串列中。如果鍵本來就是有序的,使用LinkedHashMap而非TreeMap可以提高效率。按訪問有序的特點可以方便的用於實現LRU快取。

如果鍵為列舉型別,可以使用專門的實現類EnumMap,它使用效率更高的陣列實現。

需要說明的是,我們介紹的各種容器類都不是執行緒安全的,也就是說,如果多個執行緒同時讀寫同一個容器物件,是不安全的。如果需要執行緒安全,可以使用Collections提供的synchronizedXXX方法對容器物件進行同步,或者使用執行緒安全的專門容器類。

此外,容器類提供的迭代器都有一個特點,都會在迭代中間進行結構性變化檢測,如果容器發生了結構性變化,就會丟擲ConcurrentModificationException,所以不能在迭代中間直接呼叫容器類提供的add/remove方法,如需新增和刪除,應呼叫迭代器的相關方法。

在解決一個特定問題時,經常需要綜合使用多種容器類。比如要統計一本書中出現次數最多的前10個單詞,可以先使用HashMap統計每個單詞出現的次數,再使用47節介紹的TopK類用PriorityQueue求前十個10單詞,或者使用Collections提供的sort方法。

在之前各節介紹的例子中,為簡單起見,容器中的元素型別往往是簡單的,但需要說明的是,它們也可以是複雜的自定義型別,也可以是容器型別。比如在一個新聞應用中,表示當天的前十大新聞可以用一個List表示,形如List<News>,而為了表示每個分類的前十大新聞,可以用一個Map表示,鍵為分類Category,值為List<News>,形如Map<Category, List<News>>,而表示每天的每個分類的前十大新聞,可以在Map中使用Map,鍵為日期,值也是一個Map,形如Map<Date, Map<Category, List<News>>

資料結構和演算法

在容器類中,我們看到了如下資料結構的應用:

  • 動態陣列:ArrayList內部就是動態陣列,HashMap內部的連結串列陣列也是動態擴充套件的,ArrayDeque和PriorityQueue內部也都是動態擴充套件的陣列。
  • 連結串列:LinkedList是用雙向連結串列實現的,HashMap中對映到同一個連結串列陣列的鍵值對是通過單向連結串列連結起來的,LinkedHashMap中每個元素還加入到了一個雙向連結串列中以維護插入或訪問順序。
  • 雜湊表:HashMap是用雜湊表實現的,HashSet, LinkedHashSet和LinkedHashMap基於HashMap,內部當然也是雜湊表。
  • 排序二叉樹:TreeMap是用紅黑樹(基於排序二叉樹)實現的,TreeSet內部使用TreeMap,當然也是紅黑樹,紅黑樹能保持元素的順序且綜合效能很高。
  • :PriorityQueue是用堆實現的,堆邏輯上是樹,物理上是動態陣列,堆可以高效地解決一些其他資料結構難以解決的問題。
  • 迴圈陣列:ArrayDeque是用迴圈陣列實現的,通過對頭尾變數的維護,實現了高效的佇列操作。
  • 位向量:EnumSet是用位向量實現的,對於只有兩種狀態,且需要進行集合運算的資料,使用位向量進行表示、位運算進行處理,精簡且高效。

每種資料結構中往往包含一定的演算法策略,這種策略往往是一種折中,比如:

  • 動態擴充套件演算法:動態陣列的擴充套件策略,一般是指數級擴充套件的,是在兩方面進行平衡,一方面是希望減少記憶體消耗,另一方面希望減少記憶體分配、移動和拷貝的開銷。
  • 雜湊演算法:雜湊表中鍵對映到連結串列陣列索引的演算法,演算法要快,同時要儘量隨機和均勻。
  • 排序二叉樹的平衡演算法:排序二叉樹的平衡非常重要,紅黑樹是一種平衡演算法,AVL樹是另一種,平衡演算法一方面要保證儘量平衡,另一方面要儘量減少綜合開銷。

Collections實現了一些通用演算法,比如二分查詢、排序、翻轉列表順序、隨機化重排、迴圈移位等,在實現大部分演算法時,Collections也都根據容器大小和是否實現了RandomAccess介面採用了不同的實現方式。

設計思維和模式

在容器類中,我們也看到了Java的多種語言機制和設計思維的運用:

  • 封裝:封裝就是提供簡單介面,並隱藏實現細節,這是程式設計的最重要思維。在容器類中,很多類、方法和變數都是私有的,比如迭代器方法,基本都是通過私有內部類或匿名內部類實現的。
  • 繼承和多型:繼承可以複用程式碼,便於按父類統一處理,但我們也說過,繼承是一把雙刃劍。在容器類中,Collection是父介面,List/Set/Queue繼承自Collection,通過Collection介面可以統一處理多種型別的集合物件。容器類定義了很多抽象容器類,具體類通過繼承它們以複用程式碼,每個抽象容器類都有詳細的文件說明,描述其實現機制,以及子類應該如何重寫方法。容器類的設計展示了介面繼承、類繼承、以及抽象類的恰當應用。
  • 組合:一般而言,組合應該優先於繼承,我們看到HashSet通過組合的方式使用HashMap,TreeSet通過組合使用TreeMap,介面卡和裝飾器模式也都是通過組合實現的。
  • 介面:面向介面程式設計是一種重要的思維,可降低程式碼間的耦合,提高程式碼複用程度,在容器類方法中,接受的引數和返回值往往都是介面,Collections提供的通用演算法,操作的也都是介面物件,我們平時在使用容器類時,一般也只在建立物件時使用具體類,而其他地方都使用介面。
  • 設計模式:我們在容器類中看到了迭代器、工廠方法、介面卡、裝飾器等多種設計模式的應用。

小結

本節我們從用法和特點、資料結構和演算法、以及設計思維和模式三個角度簡要總結了之前介紹的各種容器類。到此為止,關於容器類我們就介紹完了。

到目前為止,我們還沒有接觸過檔案處理,而我們在日常的電腦操作中,接觸最多的就是各種檔案了,讓我們從下一節開始,一起探討檔案操作。


未完待續,檢視最新文章,敬請關注微信公眾號“老馬說程式設計”(掃描下方二維碼),深入淺出,老馬和你一起探索Java程式設計及計算機技術的本質。用心原創,保留所有版權。

計算機程式的思維邏輯 (55) – 容器類總結

相關文章