初衷
Java集合是我們使用最頻繁的工具,也是面試的熱點,但我們對它的理解僅限於使用上,而且大多數情況沒有考慮過其使用規範。本系列文章將跟隨原始碼的思路,分析實現的每個細節,以期在使用時避免各種不規範的坑。在這裡,我們會驚豔於開發者優秀的設計,也會感激先輩們付出的艱辛努力,更重要的是知其所以然,少犯錯誤,寫出優秀的程式碼。
許多人對集合類的理解是暴力的,當需要儲存物件時就使用ArrayList
,當需要儲存鍵值對時就使用HashMap
,當需要不可重複時就使用HashSet
,等等。而且使用方式也比較單一:
List<String> list = new ArrayList<>();
Map<String, String> map = new HashMap<>();
Set<String> set = new HashSet<>();
// ...
複製程式碼
這裡我們先不考慮多執行緒安全問題,這個問題通常有專門的類實現,或者可以通過Collections.synchronizedXXX
方法解決。除此之外,我們真的可以如此簡單的使用集合嗎?
假如資料只有幾百、幾千個,那麼使用何種方式實現差別並不大。但當我們需要處理大數量級的資料時,採用不同的方式效率可能相差百倍甚至更多,這種情況下效能將變得格外重要。例如分別儲存於ArrayList
和LinkedList
的100萬條資料,要獲取位於位置 i 的元素,前者可以瞬間完成,後者則可能需要數秒。這時,使用哪個集合類,怎樣合理使用就是我們必須掌握的技能了。
為什麼要讀本系列文章
如果你也像以上這般使用集合,或者不知道如何優化集合的使用,你都應該讀本系列文章。如果你僅有一些點不清晰,也可以在這裡找到答案。或者你只是不想閱讀枯燥的原始碼,卻對原理很好奇,你也可以閱讀本系列文章。如果你只是想應付面試,我想當你堅持把這些文章讀完後,你會覺得面試好像也不那麼重要了。
本系列文章立足於深刻理解Java集合的原理與實現,讀完這些文章後你將獲得以下知識:
-
大量的資料結構知識。
-
ArrayList有那麼多建構函式,使用不同的建構函式會有區別嗎?
-
ArrayList是如何擴容的?
-
LinkedList如何提供通過位置獲取資料的功能的,它的查詢效率真的非常低嗎?
-
用陣列可以實現佇列嗎?
-
影響HashMap效能的因素有哪些?
-
複雜的紅黑樹是如何實現的?
-
LRUCache的底層原理是什麼?
-
……
基礎知識概述
對資料的操作,大抵就是增、刪、改、查,以及在某些時候根據位置獲取資料,有時可能還需要進行排序。改和查又可以理解為一致的操作,因為要修改一條資料需要先找到它,然後替換即可。接下來我們就從增、刪、查這三點簡要分析下當前使用比較廣泛的幾種資料結構。
陣列
陣列在記憶體中佔據一段連續的記憶體,所有的資料在記憶體中連續排列。它的大小是固定的,這一特性使得陣列對於插入操作並不友好,我們分析ArrayList
時就會看到這種操作的複雜。但陣列對於位置的訪問是極其友好的,它支援所謂RandomAccess
特性,這使得基於位置的操作可以迅速完成,其時間複雜度為O(1)。陣列的資料順序與插入順序一致,所以查詢操作需要遍歷,其時間複雜度為O(n)。
所以陣列最大的優勢在於基於位置的訪問,在擴充套件性方面表現無力。
連結串列
不同於陣列,連結串列是通過指標域來表示資料與資料之間的位置關係的,所以連結串列在頭部或尾部插入資料的複雜度僅為O(1)。連結串列不具備RandomAccess
特性,所以無法提供基於位置的訪問。其查詢操作也必須從從到尾遍歷,複雜度為O(n)。
所以連結串列最大的優勢在於插入,而查詢的表現很一般。
那有沒有一種結構能夠結合陣列和連結串列的優點,使得查詢和插入都具有優秀的表現呢?答案是肯定的,這就是雜湊表。
雜湊表
雜湊表就是Hash Table,這種結構使用key-value
形式儲存資料,我們經常使用的HashMap
、HashTable
就基於它。
陣列和連結串列在查詢時表現一般的原因在於它們並不記得資料的位置,所以只能用待查詢的資料和儲存的資料依次比對。雜湊表使用一種巧妙的方式來減少甚至避免這種依次比對,它的原理是通過一個函式把任何的key轉為int,每次查詢時只需要執行一次這個函式便可以迅速定位。這個過程是不是像查字典呢?
雜湊表並不像上述那般完美,因為並不會有一個函式,能夠保證所有的key轉換結果都不同,也就是會發生所謂的雜湊碰撞
,而且它必須依賴於其他的資料結構,這部分知識會在後續文章中詳細介紹。
良好設計的雜湊表可以使增、刪、查等操作的時間複雜度均為O(1)。
二叉排序樹
二叉排序樹是解決查詢問題的另一方案,如果資料在插入時是有序的,在查詢時就可以使用二分法。二分法的原理很簡單,比如猜一個在0-100之間的數,第一次猜50就可以直接排除一半的資料,每次按照這個規則就可以很快的獲取正確答案。二分法的時間複雜度為O(lg n)。
樹的結構對二分法有天然的支援(但這不是樹最重要的用途)。二叉排序樹犧牲了一部分插入的時間,但提高了查詢的速度,同時有序的資料也可以做些其他的操作。如果查詢的操作重要性超過了插入,我們應該考慮這種結構。二叉排序樹也存在一些不平衡導致效率下降的問題,所以有了AVL樹、紅黑樹,以及用於資料庫索引的B樹、B+樹等概念,關於二叉排序樹的知識也會在後續文章中介紹。
分析過程
以上介紹的資料結構的知識是我們理解Java集合類的基礎,掌握這些核心原理,我們分析集合類原始碼時才不會吃力,我們會先對這些資料結構進行簡要介紹,其他和本系列文章無關的概念不會涉及,大家可以查閱相關專業書籍進行系統學習。
由於集合類的原始碼十分龐大,從介面抽象設計到具體實現涉及到數十個類,我們不可能每行程式碼都進行分析,一些在前面分析過的點在後續部分也會略過,但對於我們應該注意的點都會詳細解讀。有一些過於複雜的程式碼,還會用圖示進行直觀的演示,以幫助理解整個執行機制。
文章中會不可避免地貼上大量原始碼,但所有部分都會加上詳細的中文註釋。另外,貼上的程式碼不會擷取(某些沒必要的會刪除),這樣便於理解,而不用想看哪行程式碼再去原始碼中尋找了。
學習原始碼的實現僅是我們的目的之一,我們更應該掌握作者優秀的程式設計思想,理解這樣做的初衷,站在更高的角度思考問題。
本系列文章的原始碼全部基於JDK1.8,不同版本的實現程式碼可能稍有差別,但核心思想是一致的,希望大家不要被具體的實現帶偏了路。
Java集合類分為兩大部分:Collection和Map。Collection又主要由List、Queue和Set三大模組組成。本系列文章也會基於這樣的結構進行,我們會先了解一些用到的資料結構,然後按照從介面抽象到具體實現的順序來一步步揭開集合的神祕面紗。
由於Set的結構與Map完全一致,且Set的內部都是基於對應的Map實現的,所以只需要知道Set是什麼即可,其具體實現如果感興趣可以自行閱讀原始碼。
本系列文章不考慮多執行緒安全問題,與多執行緒相關的問題十分複雜,以後會對它專門研究。
本系列文章長達20多篇,全部讀完需要一定的耐心,但是我相信讀完對資料結構和集合一定會有更深的理解,在使用時需要注意哪些點也一定會胸有成竹。
另外由於個人能力有限,文章中若有表達不清晰或解釋錯誤的部分,希望各位看官能夠給予批評指正。
目錄結構
本系列文章會按照下述結構搭建:
-
資料結構
-
Iterable概述
-
Collection概述
-
List系列分析
-
Queue系列分析
-
Map概述與系列分析
-
Set簡介
以下是全部文章連結:
Java集合原始碼分析之基礎(五):平衡二叉樹(AVL Tree)
Java集合原始碼分析之基礎(六):紅黑樹(RB Tree)
Java集合原始碼分析之Queue(一):超級介面Queue
Java集合原始碼分析之Queue(三):ArrayDeque
Java集合原始碼分析之Map(二):介面SortedMap
Java集合原始碼分析之Map(三):介面NavigableMap
Java集合原始碼分析之Map(六):LinkedHashMap
本系列文章全部更新完畢,感謝您的關注~
本文到此就結束了,如果您喜歡我的文章,可以關注我的微信公眾號:大大紙飛機
或者掃描下方二維碼直接新增:
您也可以關注我的github:https://github.com/LtLei/articles
程式設計之路,道阻且長。唯,路漫漫其修遠兮,吾將上下而求索。