集合框架中包含了一系列不一樣數據結構(線性表,查找表...),是用來保存一組數據的結構。html
整個集合框架關係展示
java
原圖出處:http://pierrchen.blogspot.com/2014/03/java-collections-framework-cheat-sheet.htmlnode
處於圖片左上角的那一塊灰色裏面的四個類(Dictionary、HashTable、Vector、Stack)都是線程安全的,但是它們都是JDK的老的遺留類。現在都有了相應的取代類。面試
當中Map接口是用來取代圖片中左上角的那個Dictionary抽象類。算法
HashTable,官方推薦ConcurrentHashMap來取代。接着如下的Vector是List如下的一個實現類。數組
最上面的粉紅色部分是集合類所有接口關係圖。其中Collection有三個繼承接口:List、Queue和Set。緩存
綠色部分則是集合類的主要實現類,也是咱們經常使用的集合類。安全
在這裏,集合類分爲了Map和Collection兩個大的類別。數據結構
1) Collectionapp
一組"對立"的元素,一般這些元素都服從某種規則
1.1) List必須保持元素特定的順序
1.2) Set不能有重複元素
1.3) Queue保持一個隊列(先進先出)的順序
2) Map
一組成對的"鍵值對"對象
集合分類:
依照實現接口分類:
實現Map接口的有:EnumMap、IdentityHashMap、HashMap、LinkedHashMap、WeakHashMap、TreeMap
實現List接口的有:ArrayList、LinkedList
實現Set接口的有:HashSet、LinkedHashSet、TreeSet
實現Queue接口的有:PriorityQueue、LinkedList、ArrayQueue
依據底層實現的數據結構分類:
底層以數組的形式實現:EnumMap、ArrayList、ArrayQueue
底層以鏈表的形式實現:LinkedHashSet、LinkedList、LinkedHashMap
底層以hash table的形式實現:HashMap、HashSet、LinkedHashMap、LinkedHashSet、WeakHashMap、IdentityHashMap
底層以紅黑樹的形式實現:TreeMap、TreeSet
底層以二叉堆的形式實現:PriorityQueue
Collection經常使用方法
int size():返回集合裏邊包含的對象個數
boolean isEmpty():是否爲空(不是null而是裏邊沒有元素)
boolean contains(Object o):是否包含指定對象
boolean clear():清空集合
boolean add(E e):向集合中添加對象
boolean remove(Object o):移出某個對象
boolean addAll(Collection <?extends E> c):將另外一個集合中的全部元素添加到集合中。
boolean removeAll(Collection<?> c):移出集合中與另外一個集合中相同的所有元素。
Iterator<E> iterator():返回該集合的對應的迭代器。
list經常使用方法
List除了繼承Collection定義的方法外,還根據線性表的數據結構定義了一系列方法。
1)get(int index)方法,獲取集合中索引的元素。
注:這個方法是List中獨有的,返回的是Object
2)Object set(int index,Object obj):將給定的元素替換集合中索引爲index的元素,返回的是被替換的元素。
3)add和remove有方法重載
add(int index, Object obj):將給定的元素插入索引處,原位置上及後面的元素順序向後移(插隊)。
Object remove(int index):刪除指定索引處的元素,該方法的返回只是被刪除的元素。
List還提供相似String的indexOf和lastIndexOf方法,用於在集合中檢索某個對象,其判斷邏輯爲:(o==null?get(i)==null:o.equals(get(i)))
1)int indexOf(Object obj):返回首次在集合中出現該元素的索引值。
2)lastIndexOf(Object obj):返回最後一次在集合中出現該元素的索引值。
還有能夠將集合轉換爲數組的方法:
3)toArray():將集合轉化爲數組。這裏參數僅僅是告知集合要轉換的數組類型,並不會使用咱們提供的數組,因此不須要給長度。
集合中的元素應爲同一個類型。
String[] array = (String[])list.toArray(new String[0]);
ArrayList底層實現方式
ArrayList底層是用數組實現的存儲。 特色:查詢效率高,增刪效率低,線程不安全。
ArrayList底層使用Object數組來存儲元素數據。全部的方法,都圍繞這個核心的Object數組來操做。
可是,數組長度是有限的,而ArrayList是能夠存聽任意數量的對象,長度不受限制。
其本質上就是經過定義新的更大的數組,將舊數組中的內容拷貝到新數組,來實現擴容。 ArrayList的Object數組初始化長度爲10,若是咱們存儲滿了這個數組,須要存儲第11個對象,就會定義新的長度更大的數組,並將原數組內容和新的元素一塊兒加入到新數組中。
LinkedList底層實現
LinkedList底層用雙向鏈表實現的存儲。特色:查詢效率低,增刪效率高,線程不安全。
雙向鏈表也叫雙鏈表,是鏈表的一種,它的每一個數據節點中都有兩個指針,分別指向前一個節點和後一個節點。 因此,從雙向鏈表中的任意一個節點開始,均可以很方便地找到全部節點。每一個節點都應該有3部份內容:
class Node { Node previous; //前一個節點 Object element; //本節點保存的數據 Node next; //後一個節點 }
private static class Node<E> { //業務數據 E item; //指向下個node Node<E> next; //指向上個node Node<E> prev; Node(Node<E> prev, E element, Node<E> next) { this.item = element; this.next = next; this.prev = prev; } }
若是原來firstNode爲空的話,說明這個list爲空,那麼這時FirstNode也就是lastNode,這個鏈表只有一個node。
首節點的prev和lastNode的next爲null
HashMap實現原理
Map就是用來存儲「鍵(key)-值(value) 對」的。 Map類中存儲的「鍵值對」經過鍵來標識,因此「鍵對象」不能重複。
哈希表
哈希表(hash table)也叫散列表,是一種很是重要的數據結構,應用場景及其豐富,許多緩存技術(好比memcached)的核心其實就是在內存中維護一張大的哈希表,而HashMap的實現原理也經常出如今各種的面試題中,重要性可見一斑。
HashMap底層實現採用了哈希表,這是一種很是重要的數據結構。
數據結構中由數組和鏈表來實現對數據的存儲,他們各有特色。
(1) 數組:佔用空間連續。 尋址容易,查詢速度快。可是,增長和刪除效率很是低。
(2) 鏈表:佔用空間不連續。 尋址困難,查詢速度慢。可是,增長和刪除效率很是高。
而「哈希表」具有了數組和鏈表的優勢。 哈希表的本質就是「數組+鏈表」。
在哈希表中進行添加,刪除,查找等操做,性能十分之高,不考慮哈希衝突的狀況下,僅需一次定位便可完成,時間複雜度爲O(1),接下來咱們就來看看哈希表是如何實現達到驚豔的常數階O(1)的。
數據結構的物理存儲結構只有兩種:順序存儲結構和鏈式存儲結構(像棧,隊列,樹,圖等是從邏輯結構去抽象的,映射到內存中,也這兩種物理組織形式),而在數組中根據下標查找某個元素,一次定位就能夠達到,哈希表利用了這種特性,哈希表的主幹就是數組。
好比咱們要新增或查找某個元素,咱們經過把當前元素的關鍵碼經過某個函數映射到數組中的某個位置,經過數組下標一次定位就可完成操做。
存儲位置 = f(關鍵碼)
其中,這個函數f通常稱爲哈希函數,經過關鍵碼就能夠直接定位到元素的存儲位置。
哈希衝突
若是兩個不一樣的元素,經過哈希函數得出的實際存儲地址相同怎麼辦?也就是說,當咱們對某個元素進行哈希運算,獲得一個存儲地址,而後要進行插入的時候,發現已經被其餘元素佔用了,其實這就是所謂的哈希衝突,也叫哈希碰撞。哈希函數的設計相當重要,好的哈希函數會盡量地保證 計算簡單和散列地址分佈均勻。可是,咱們須要清楚的是,數組是一塊連續的固定長度的內存空間,再好的哈希函數也不能保證獲得的存儲地址絕對不發生衝突。那麼哈希衝突如何解決呢?哈希衝突的解決方案有多種:開放定址法(發生衝突,繼續尋找下一塊未被佔用的存儲地址),再散列函數法,鏈地址法,而HashMap便是採用了鏈地址法,也就是數組+鏈表的方式。
或者
HashMap的主幹是一個Entry數組。Entry是HashMap的基本組成單元,每個Entry包含一個key-value鍵值對。
一個Entry對象存儲了:
1. key:鍵對象 value:值對象
2. next:下一個節點
3. hash: 鍵對象的hash值
存儲數據
咱們的目的是將」key-value兩個對象」成對存放到HashMap的Entry[]數組中。
(1) 得到key對象的hashcode
首先調用key對象的hashcode()方法,得到hashcode。
(2) 根據hashcode計算出hash值(要求在[0, 數組長度-1]區間)
hashcode是一個整數,咱們須要將它轉化成[0, 數組長度-1]的範圍。咱們要求轉化後的hash值儘可能均勻地分佈在[0,數組長度-1]這個區間,減小「hash衝突」
i. 一種極端簡單和低下的算法是:
hash值 = hashcode/hashcode;
也就是說,hash值老是1。意味着,鍵值對對象都會存儲到數組索引1位置,這樣就造成一個很是長的鏈表。至關於每存儲一個對象都會發生「hash衝突」,HashMap也退化成了一個「鏈表」。
ii. 一種簡單和經常使用的算法是(相除取餘算法):
hash值 = hashcode%數組長度
這種算法可讓hash值均勻的分佈在[0,數組長度-1]的區間。 早期的HashTable就是採用這種算法。可是,這種算法因爲使用了「除法」,效率低下。JDK後來改進了算法。首先約定數組長度必須爲2的整數冪,這樣採用位運算便可實現取餘的效果:hash值 = hashcode&(數組長度-1)。
(3) 生成Entry對象
如上所述,一個Entry對象包含4部分:key對象、value對象、hash值、指向下一個Entry對象的引用。咱們如今算出了hash值。下一個Entry對象的引用爲null。
(4) 將Entry對象放到table數組中
若是本Entry對象對應的數組索引位置尚未放Entry對象,則直接將Entry對象存儲進數組。若是對應索引位置已經有Entry對象,則將已有Entry對象的next指向本Entry對象,造成鏈表。
總結:
當添加一個元素(key-value)時,首先計算key的hash值,以此肯定插入數組中的位置,可是可能存在同一hash值的元素已經被放在數組同一位置了,這時就添加到同一hash值的元素的後面,他們在數組的同一位置,就造成了鏈表,同一個鏈表上的Hash值是相同的,因此說數組存放的是鏈表。
▪ 取數據過程get(key)
咱們須要經過key對象得到「鍵值對」對象,進而返回value對象。明白了存儲數據過程,取數據就比較簡單了,參見如下步驟:
(1) 得到key的hashcode,經過hash()散列算法獲得hash值,進而定位到數組的位置。
(2) 在鏈表上挨個比較key對象。 調用equals()方法,將key對象和鏈表上全部節點的key對象進行比較,直到碰到返回true的節點對象爲止。
(3) 返回equals()爲true的節點對象的value對象。
明白了存取數據的過程,咱們再來看一下hashcode()和equals方法的關係:
Java中規定,兩個內容相同(equals()爲true)的對象必須具備相等的hashCode。由於若是equals()爲true而兩個對象的hashcode不一樣;那在整個存儲過程當中就發生了悖論。
▪ 擴容問題
HashMap的位桶數組,初始大小爲16。實際使用時,顯然大小是可變的。若是位桶數組中的元素達到(0.75*數組 length), 就從新調整數組大小變爲原來2倍大小。
擴容很耗時。擴容的本質是定義新的更大的數組,並將舊數組內容挨個拷貝到新數組中。
▪ JDK8將鏈表在大於8狀況下變爲紅黑二叉樹
JDK8中,HashMap在存儲一個元素時,當對應鏈表長度大於8時,鏈表就轉換爲紅黑樹,這樣又大大提升了查找的效率。
HashMap原理借鑑https://www.cnblogs.com/chengxiao/p/6059914.html
TreeMap原理實現
首先介紹一下二叉樹和紅黑二叉樹
二叉樹的定義
二叉樹是樹形結構的一個重要類型。 許多實際問題抽象出來的數據結構每每是二叉樹的形式,即便是通常的樹也能簡單地轉換爲二叉樹,並且二叉樹的存儲結構及其算法都較爲簡單,所以二叉樹顯得特別重要。
二叉樹(BinaryTree)由一個節點及兩棵互不相交的、分別稱做這個根的左子樹和右子樹的二叉樹組成。下圖中展示了五種不一樣基本形態的二叉樹。
(a) 爲空樹。
(b) 爲僅有一個結點的二叉樹。
(c) 是僅有左子樹而右子樹爲空的二叉樹。
(d) 是僅有右子樹而左子樹爲空的二叉樹。
(e) 是左、右子樹均非空的二叉樹。
注:二叉樹的左子樹和右子樹是嚴格區分而且不能隨意顛倒的,圖 (c) 與圖 (d) 就是兩棵不一樣的二叉樹。
排序二叉樹特性以下:
(1) 左子樹上全部節點的值均小於它的根節點的值。
(2) 右子樹上全部節點的值均大於它的根節點的值。
好比:咱們要將數據【14,12,23,4,16,13, 8,,3】存儲到排序二叉樹中,以下圖所示:
排序二叉樹自己實現了排序功能,能夠快速檢索。但若是插入的節點集自己就是有序的,要麼是由小到大排列,要麼是由大到小排列,那麼最後獲得的排序二叉樹將變成普通的鏈表,其檢索效率就會不好。 好比上面的數據【14,12,23,4,16,13, 8,,3】,咱們先進行排序變成:【3,4,8,12,13,14,16,23】,而後存儲到排序二叉樹中,顯然就變成了鏈表,以下圖所示:
平衡二叉樹(AVL)
爲了不出現上述一邊倒的存儲,科學家提出了「平衡二叉樹」。
在平衡二叉樹中任何節點的兩個子樹的高度最大差異爲1,因此它也被稱爲高度平衡樹。 增長和刪除節點可能須要經過一次或屢次樹旋轉來從新平衡這個樹。
節點的平衡因子是它的左子樹的高度減去它的右子樹的高度(有時相反)。帶有平衡因子一、0或 -1的節點被認爲是平衡的。帶有平衡因子 -2或2的節點被認爲是不平衡的,並須要從新平衡這個樹。
好比,咱們存儲排好序的數據【3,4,8,12,13,14,16,23】,增長節點若是出現不平衡,則經過節點的左旋或右旋,從新平衡樹結構,最終平衡二叉樹以下圖所示:
平衡二叉樹追求絕對平衡,實現起來比較麻煩,每次插入新節點須要作的旋轉操做次數不能預知。
紅黑二叉樹
紅黑二叉樹(簡稱:紅黑樹),它首先是一棵二叉樹,同時也是一棵自平衡的排序二叉樹。
紅黑樹在原有的排序二叉樹增長了以下幾個要求:
1. 每一個節點要麼是紅色,要麼是黑色。
2. 根節點永遠是黑色的。
3. 全部的葉節點都是空節點(即 null),而且是黑色的。
4. 每一個紅色節點的兩個子節點都是黑色。(從每一個葉子到根的路徑上不會有兩個連續的紅色節點)
5. 從任一節點到其子樹中每一個葉子節點的路徑都包含相同數量的黑色節點。
這些約束強化了紅黑樹的關鍵性質:從根到葉子的最長的可能路徑很少於最短的可能路徑的兩倍長。這樣就讓樹大體上是平衡的。
紅黑樹是一個更高效的檢索二叉樹,JDK 提供的集合類 TreeMap、TreeSet 自己就是一個紅黑樹的實現。
紅黑樹的基本操做:插入、刪除、左旋、右旋、着色。 每插入或者刪除一個節點,可能會致使樹不在符合紅黑樹的特徵,須要進行修復,進行 「左旋、右旋、着色」操做,使樹繼續保持紅黑樹的特性。
TreeMap是紅黑二叉樹的典型實現
private transient Entry<K,V> root = null;
root用來存儲整個樹的根節點。咱們繼續跟蹤Entry(是TreeMap的內部類)的代碼:
能夠看到裏面存儲了自己數據、左節點、右節點、父節點、以及節點顏色。
TreeMap的put()/remove()方法大量使用了紅黑樹的理論。
TreeMap和HashMap實現了一樣的接口Map,所以,用法對於調用者來講沒有區別。HashMap效率高於TreeMap;在須要排序的Map時才選用TreeMap。
HashSet實現原理
HashSet是採用哈希算法實現,底層實際是用HashMap實現的(HashSet本質就是一個簡化版的HashMap),所以,查詢效率和增刪效率都比較高。發現裏面有個map屬性,這就是HashSet的核心祕密。咱們再看add()方法,發現增長一個元素說白了就是在map中增長一個鍵值對,鍵對象就是這個元素,值對象是名爲PRESENT的Object對象。
本質就是把這個元素做爲key加入到了內部的map中」。
因爲map中key都是不可重複的,所以,Set自然具備「不可重複」的特性。
TreeSet實現原理
TreeSet底層實際是用TreeMap實現的,內部維持了一個簡化版的TreeMap,經過key來存儲Set的元素。 TreeSet內部須要對存儲的元素進行排序,所以,咱們對應的類須要實現Comparable接口。這樣,才能根據compareTo()方法比較對象之間的大小,才能進行內部排序。
(1) 因爲是二叉樹,須要對元素作內部排序。 若是要放入TreeSet中的類沒有實現Comparable接口,則會拋出異常:java.lang.ClassCastException。
(2) TreeSet中不能放入null元素。