Collection & Map
Collection 子類有 List 和 Set
List --> ArrayList / LinkedList / Vector
Set --> HashSet / TreeSet
Map --> HashMap / HashTable / TreeMap / LinkedHashMap
一、ArrayList
ArrayList 是 List 接口的可變數組的實現。實現了所有可選列表操作,并允許包括 null 在內的所有元素。除了實現 List 接口外, 此類還提供一些方法來操作內部用來存儲列表的數組的大小。
每個 ArrayList 實例都有一個容量,該容量是指用來存儲列表元素的數組的大小。它總是至少等于列表的大小。隨著向 ArrayList 中不斷添加元素,其容量也自動增長 (每次調用添加操作時,都會調用 ensureCapacity 方法,判斷是否需要自增,如果需要則自增數組) 。自動增長會帶來數據向新數組的重新拷貝,因此,如果可預知數據量的多少,可在構造 ArrayList 時指定其容量。在添加大量元素前,應用程序也可以使用 ensureCapacity 操作來增加 ArrayList 實例的容量,這可以減少遞增式再分配的數量。
注意,此實現不是同步的。如果多個線程同時訪問一個 ArrayList 實例,而其中至少一個線程從結構上修改了列表,那么它必須保持外部同步。(結構上的修改是指任何添加或刪除一個或多個元素的操作,或者顯式調整底層數組的大小;僅僅設置元素的值不是結構上的修改。)
不管是 ArrayList、 Vector、LinkedList 他們的 set,remove 方法的返回值都是原來該位置的元素,add 方法返回 boolean 值為是否成功插入
1、實現的接口
繼承 AbstractList (實現了 List 接口)
Cloneable 可克隆, Serializable 可序列化,RandomAccess 為 List 提供快速訪問功能(RandomAccess 為空接口,只是一個可以快速訪問的標識),即通過序號獲取元素
2、構造方法
創建長度為 10 的數組
創建指定長度數組,小于 0 拋出異常
根據集合創建數組,創建長度為集合長度的數組并拷貝
3、增刪查方法
每次操作之前都會創建一個新的數組引用指向被操作數組,使用新的引用操作。
set 方法,指定位置賦值,檢查 index ,如果不合法則拋出異常
add 方法,末尾位置添加,如果超出,先創建新數組替換舊數組,新數組長度為舊數組的 1.5 倍再加 1;
add(int index,Object obj) 指定位置添加,檢查 index ,如不合法則拋出異常。指定位置插入時,會將原來的數組以 index 為界,將 index 后的數據后移一位,后移的實現通過 System.arraycopy 方法實現。再在 index 位置插入需要插入的數據。 System.arraycopy 為 Native 層的方法,可以高效復制數組元素。
remove(int index) 根據索引刪除,直接操作數組,返回值為被移除的對象。將該對象所在位置之后的數組內容復制到從該位置開始,將末尾置為 null
remove(Object obj) 根據對象刪除,遍歷數組,如果存在,將該對象所在位置之后的數組內容復制到從該位置開始,將末尾置為 null
當我們可預知要保存的元素的多少時,要在構造 ArrayList 實例時,就指定其容量,以避免數組擴容的發生。或者根據實際需求,通過調用ensureCapacity 方法來手動增加 ArrayList 實例的容量。
ArrayList基于數組實現,可以通過下標索引直接查找到指定位置的元素,因此查找效率高,但每次插入或刪除元素,就要大量地移動元素,插入刪除元素的效率低。
在查找給定元素索引值等的方法中,源碼都將該元素的值分為null和不為null兩種情況處理,ArrayList中允許元素為null。
二、Vector
- Vector也是基于數組實現的,是一個動態數組,其容量能自動增長。
- Vector是JDK1.0引入了,它的很多實現方法都加入了同步語句,使用 synchronized 修飾,因此是線程安全的(其實也只是相對安全,有些時候還是要加入同步語句來保證線程的安全),可以用于多線程環境。
- Vector沒有實現 Serializable 接口,因此它不支持序列化,實現了 RandomAccess
- Vector 的構造函數中可以指定容量增長系數,如果不指定增長系數,增加時為增加一倍,這點有別于 ArrayList。
Vector的源碼實現總體與ArrayList類似,關于Vector的源碼,給出如下幾點總結:
1、Vector有四個不同的構造方法。無參構造方法的容量為默認值10,僅包含容量的構造方法則將容量增長量(從源碼中可以看出容量增長量的作用,第二點也會對容量增長量詳細說)明置為0。
2、注意擴充容量的方法ensureCapacityHelper。與ArrayList相同,Vector在每次增加元素(可能是1個,也可能是一組)時,都要調用該方法來確保足夠的容量。當容量不足以容納當前的元素個數時,就先看構造方法中傳入的容量增長量參數CapacityIncrement是否為0,如果不為0,就設置新的容量為就容量加上容量增長量,如果為0,就設置新的容量為舊的容量的2倍,如果設置后的新容量還不夠,則直接新容量設置為傳入的參數(也就是所需的容量),而后同樣用Arrays.copyof()方法將元素拷貝到新的數組。
3、很多方法都加入了synchronized同步語句,來保證線程安全。
4、同樣在查找給定元素索引值等的方法中,源碼都將該元素的值分為null和不為null兩種情況處理,Vector中也允許元素為null
5、其他很多地方都與ArrayList實現大同小異,Vector現在已經基本不再使用。
三、LinkedList
LinkedList 和 ArrayList 一樣,實現了 List 接口,但其內部的數據結構有本質不同。LinkedList 是基于雙向循環鏈表實現的,所以它的插入和刪除操作比 ArrayList 更高效,不過由于是基于鏈表的,隨機訪問的效率要比 ArrayList 差。
實現了 Searializable 接口,支持序列化,實現了 Cloneable 接口,可被克隆
是非線程安全的,只是用于單線程環境下,多線程環境下可以采用concurrent并發包下的concurrentHashMap。
1、數據結構
LinkedList 是基于鏈表結構的實現,每一個節點的類都包含了 previous 和 next 兩個 Link 指針對象,由 Link 保存,Link 中包含了上一個節點和下一個節點的引用,這樣就構成了雙向的鏈表,每個 Link 只能知道自己的前一個和后一個節點。
注意:不同版本類名不同,但是原理一樣,有的版本類名是 Node
Link
private static final class Link<ET> {
ET data;
Link<ET> previous, next;
Link(ET o, Link<ET> p, Link<ET> n) {
data = o;
previous = p;
next = n;
}
}
2、插入數據
LinkedList 內部的 Link 對象 voidLink ,其 previous 執向鏈表最后一個對象,next 指向第一個鏈表第一個對象,初始化 LinkedList 時默認初始化 voidLink 的前后都指向自己。
注意兩個不同的構造方法。無參構造方法直接建立一個僅包含head節點的空鏈表,包含Collection的構造方法,先調用無參構造方法建立一個空鏈表,而后將Collection中的數據加入到鏈表的尾部后面。
往最后插入,會創建新的 Link 對象,并將 新對象的 previous 賦值為 voidLind 的 previous,將新對象的 next 賦值為 voidLink,最后將 voidLink 的 previous 指向 新對象,將之前 voidLind 的 previous 的對象的 next 指向新對象
往非末尾插入,會比較 index 與鏈表的中間值的大小,縮小檢索比例,調用從后往前檢索或從前往后檢索,如果從前往后,會循環調用 voidLink 的 next 方法
直到需要插入的位置得到當前位置的元素 link (注意,voidLink的 next 指向第一個元素,所以遍歷next之后的位置為需要插入的位置),創建新對象,新對象的 previous 指向原來當前元素 link 的 previous ,新對象的 next 指向 link,link 的 previous 執向新對象,原來 link 的 previous 對象的 next 指向 新元素,這樣就準確插入。從后往前的道理相同。LinkedList 獲取非首尾元素時,也會使用與插入時相同的判斷位置的加速機制
在查找和刪除某元素時,源碼中都劃分為該元素為 null 和不為 null 兩種情況來處理,LinkedList 支持插入的元素為 null
LinkedList是基于鏈表實現的,因此不存在容量不足的問題,所以這里沒有擴容的方法。
LinkedList是基于鏈表實現的,因此插入刪除效率高,查找效率低(雖然有一個加速動作)。
要注意源碼中還實現了棧和隊列的操作方法,因此也可以作為棧、隊列和雙端隊列來使用 push(向頂部插入元素)、pop(刪除并返回第一個元素) 等方法。
Iterator 中通過元素索引是否等于“雙向鏈表大小”來判斷是否達到最后。
四、HashMap
HashMap 是基于哈希表實現的,每一個元素是一個 key-value 對,其內部通過 單鏈表 解決沖突問題,容量不足(超過了閥值)時,同樣會自動增長。
HashMap是非線程安全的,只是用于單線程環境下,多線程環境下可以采用concurrent并發包下的concurrentHashMap。
HashMap 實現了Serializable接口,因此它支持序列化,實現了Cloneable接口,能被克隆
默認長度 16 擴容為 2 倍每次,如果擴容后還是不夠則創建目標長度數組,將舊數組復制到新數組中
實現方式為數組,每個數組中都可以是一個單鏈表,插入時,根據 hashcode 計算在數組中位置,判斷是否存在相同元素后,根據情況在相應位置的鏈表頭中插入新元素。
初始容量: 初始哈希數組的長度,默認 16
最大容量: 2 的 30 次冪
加載因子: 默認 0.75
閾值:用于判斷是否需要調整 HashMap 容量,等于 容量 * 加載因子
總結
加載因子,如果加載因子越大,對空間的利用更充分,但是查找效率會降低(鏈表長度會越來越長);如果加載因子太小,那么表中的數據將過于稀疏(很多空間還沒用,就開始擴容了),對空間造成嚴重浪費。如果我們在構造方法中不指定,則系統默認加載因子為0.75,這是一個比較理想的值,一般情況下我們是無需修改的。
最大容量,無論我們指定的容量為多少,構造方法都會將實際容量設為不小于指定容量的2的次方的一個數,且最大值不能超過2的30次方。要求為 2 的整數次冪是為了使不同hash值發生碰撞的概率較小,這樣就能使元素在哈希表中均勻地散列。
HashMap中key和value都允許為null。
HashMap 的數據結構
HashMap 中的數組就是哈希表,也稱為哈希數組,數組的每個元素都是一個單鏈表的頭節點,鏈表是用來解決沖突的,如果不同的key映射到了數組的同一位置處,就將其放入單鏈表中。
數組中的每一個元素都是一個 HashMapEntry,也是一個單鏈表的表頭,其 next 指向鏈表中下一元素。通過 HashMapEntry 中的 key 可以計算出其在數組也就是哈希數組中的位置,得到該位置之后,就可以在鏈表中根據 key 的 equals 方法確定某元素
其中還有一個成員遍歷 entryForNullKey ,表示 key 為 null 的元素,訪問和修改 key 為 null 的元素時,直接操作該值,之前是將 key 為 null 的元素放到了數組的第一個位置中的鏈表中,不同版本處理不同
static class HashMapEntry<K, V> implements Entry<K, V> {
final K key;
V value;
final int hash;
HashMapEntry<K, V> next;
HashMapEntry(K key, V value, int hash, HashMapEntry<K, V> next) {
this.key = key;
this.value = value;
this.hash = hash;
this.next = next;
}
public final K getKey() {
return key;
}
public final V getValue() {
return value;
}
public final V setValue(V value) {
V oldValue = this.value;
this.value = value;
return oldValue;
}
@Override public final boolean equals(Object o) {
if (!(o instanceof Entry)) {
return false;
}
Entry<?, ?> e = (Entry<?, ?>) o;
return Objects.equal(e.getKey(), key)
&& Objects.equal(e.getValue(), value);
}
@Override public final int hashCode() {
return (key == null ? 0 : key.hashCode()) ^
(value == null ? 0 : value.hashCode());
}
@Override public final String toString() {
return key + "=" + value;
}
}
插入 put(K key, V value)
@Override public V put(K key, V value) {
if (key == null) {
return putValueForNullKey(value);
}
int hash = Collections.secondaryHash(key);
HashMapEntry<K, V>[] tab = table;
int index = hash & (tab.length - 1);
for (HashMapEntry<K, V> e = tab[index]; e != null; e = e.next) {
if (e.hash == hash && key.equals(e.key)) {
preModify(e);
V oldValue = e.value;
e.value = value;
return oldValue;
}
}
// No entry for (non-null) key is present; create one
modCount++;
if (size++ > threshold) {
tab = doubleCapacity(); // 每次加入鍵值對時,都要判斷當前已用的槽的數目是否大于等于閥值(容量*加載因子),如果大于等于,則進行擴容,將容量擴為原來容量的2倍。
index = hash & (tab.length - 1);
}
addNewEntry(key, value, hash, index);
return null;
}
void addNewEntry(K key, V value, int hash, int index) {
table[index] = new HashMapEntry<K, V>(key, value, hash, table[index]);
}
如果 key 為 null 且存在 key 為 null 的元素,如果沒有則在數組中添加一個 key 為 null 的元素,如果有 key 為 null 的元素,則將該元素對應的 value 設置為新的 value
key 不為 null 情況下,由 key 計算 hash 值,由 hash 值確定該元素在哈希數組中的位置
如果當前哈希數組該位置中有值,且在當前鏈表中有 key 與新元素的 key 相同的元素(hash 值一樣,equals 也一樣) 說明是修改,則將舊元素的 value 更新為新的 value ,并將舊的 value 返回,put 方法結束
如果當前哈希數組該元素為 null 或者當前位置的鏈表中沒有 key 與新元素 key 相同的元素,那么就是插入操作。HashMap 中元素總數量加一,執行插入方法
插入時,構造新的 HashEntry ,如果當前位置為 null 就直接放入數組,如果該位置不為 null ,就講新 HashEntry 的 next 指向當前位置的 HashEntry ,并將數組當前位置賦值為新的 HashEntry,插入結束。插入成功返回值為 null。說明每次put鍵值對的時候,總是將新的該鍵值對放在table[bucketIndex]處(即頭結點處)。
刪除 remove(Object key)
如果 key 為 null 則直接將 key 為 null 位置置為 null,并將其 value 返回
如果 key 不為 null,則根據 key 計算 hash 值再計算在哈希數組中的位置
如果當前位置 HashEntry 不為 null,則遍歷單鏈表,找到元素的 key 與要刪除的 key 相同的元素,將其上一位置的 next 指向其 next,將該元素從鏈表中移除并返回
如果當前位置為 null,或者遍歷完鏈表沒有 key 匹配的元素,直接返回 null
@Override public V remove(Object key) {
if (key == null) {
return removeNullKey();
}
int hash = Collections.secondaryHash(key);
HashMapEntry<K, V>[] tab = table;
int index = hash & (tab.length - 1);
for (HashMapEntry<K, V> e = tab[index], prev = null;
e != null; prev = e, e = e.next) {
if (e.hash == hash && key.equals(e.key)) {
if (prev == null) {
tab[index] = e.next;
} else {
prev.next = e.next;
}
modCount++;
size--;
postRemove(e);
return e.value;
}
}
return null;
}
查詢 get(Object key)
public V get(Object key) {
if (key == null) {
HashMapEntry<K, V> e = entryForNullKey;
return e == null ? null : e.value;
}
int hash = Collections.secondaryHash(key);
HashMapEntry<K, V>[] tab = table;
for (HashMapEntry<K, V> e = tab[hash & (tab.length - 1)];
e != null; e = e.next) {
K eKey = e.key;
if (eKey == key || (e.hash == hash && key.equals(eKey))) {
return e.value;
}
}
return null;
}
由插入和刪除的分析,查詢就比較簡單了,不再分析
四、HashTable
Hashtable同樣是基于哈希表實現的,同樣每個元素是一個key-value對,其內部也是通過單鏈表解決沖突問題,容量不足(超過了閥值)時,同樣會自動增長。
Hashtable也是JDK1.0引入的類,是線程安全的,能用于多線程環境中。
Hashtable同樣實現了Serializable接口,它支持序列化,實現了Cloneable接口,能被克隆。
針對Hashtable,我們同樣給出幾點比較重要的總結,但要結合與HashMap的比較來總結
二者的存儲結構和解決沖突的方法都是相同的。
HashTable在不指定容量的情況下的默認容量為11,而HashMap為16,Hashtable不要求底層數組的容量一定要為2的整數次冪,而HashMap則要求一定為2的整數次冪
Hashtable中key和value都不允許為null,而HashMap中key和value都允許為null(key只能有一個為null,而value則可以有多個為null)。但是如果在Hashtable中有類似put(null,null)的操作,編譯同樣可以通過,因為key和value都是Object類型,但運行時會拋出NullPointerException異常,這是JDK的規范規定的。
Hashtable擴容時,將容量變為原來的2倍加1,而HashMap擴容時,將容量變為原來的2倍。
Hashtable計算hash值,直接用key的hashCode(),而HashMap重新計算了key的hash值,Hashtable在求hash值對應的位置索引時,用取模運算,而HashMap在求位置索引時,則用與運算。取模運算開銷比較大
五、LinkedHashMap
LinkedHashMap 是 HashMap 的子類,是有序的,放入順序和訪問順序兩種初始化方式
LinkedHashMap 可以用來實現LRU算法
LinkedHashMap 同樣是非線程安全的,只在單線程環境下使用
LinkedHashMap 中有個 boolean accessOrder 成員變量,表示雙向鏈表中元素排序規則的標志位。accessOrder為false,表示按插入順序排序,accessOrder為true,表示按訪問順序排序
數據結構
實際上就是 HashMap 和 LinkedList 兩個集合類的存儲結構的結合。在 LinkedHashMapMap 中,所有 put 進來的 Entry 都保存在哈希表中,但它又額外定義了一個 head 為頭結點的空的雙向循環鏈表,每次 put 進來 HashMapEntry ,除了將其保存到對哈希表中對應的位置上外,還要將其插入到雙向循環鏈表的尾部。
static class LinkedEntry<K, V> extends HashMapEntry<K, V> {
LinkedEntry<K, V> nxt;
LinkedEntry<K, V> prv;
/** Create the header entry */
LinkedEntry() {
super(null, null, 0, null);
nxt = prv = this;
}
/** Create a normal entry */
LinkedEntry(K key, V value, int hash, HashMapEntry<K, V> next,
LinkedEntry<K, V> nxt, LinkedEntry<K, V> prv) {
super(key, value, hash, next);
this.nxt = nxt;
this.prv = prv;
}
}
LinkedHashMap 中元素的類型為 LinkedEntry,其繼承了 HashMapEntry,并添加了 nxt、prv 兩個成員,指向該元素在雙鏈表中的前后節點
put 方法
LinkedHashMap 并沒有重寫 put 方法,而是重寫了 HashMap 中添加元素時調用的 preModify 方法和 addNewEntry 方法,proModify 在 HashMap 為空實現,在 LinkedHashMap 中調用了 makeTail 方法,接著來看:
preModify 方法是加入元素時判斷有對應 key 元素存在的情況執行的方法,說明原來的哈希數組中跟雙向鏈表中有該元素,在哈希數組中會直接修改該元素的 value 值,但是雙鏈表中的處理確有不同。此時會先將鏈表中該處的元素移除,再重新將元素的 value 賦值,之后重新將元素接到鏈表的尾端。
// 先將鏈表中該處的元素移除,再重新將元素的 value 賦值,之后重新將元素接到鏈表的尾端
private void makeTail(LinkedEntry<K, V> e) {
// Unlink e
e.prv.nxt = e.nxt;
e.nxt.prv = e.prv;
// Relink e as tail
LinkedEntry<K, V> header = this.header;
LinkedEntry<K, V> oldTail = header.prv;
e.nxt = header;
e.prv = oldTail;
oldTail.nxt = header.prv = e;
modCount++;
}
// 如果原鏈表中沒有匹配的 key 對應的元素,則直接將新元素添加到尾端
// 并將新元素的 next 執行原來哈希數組中該位置元素,并為哈希數組中對應位置賦值為新元素
@Override void addNewEntry(K key, V value, int hash, int index) {
LinkedEntry<K, V> header = this.header;
// Remove eldest entry if instructed to do so.
LinkedEntry<K, V> eldest = header.nxt;
if (eldest != header && removeEldestEntry(eldest)) { // removeEldestEntry 默認返回 false,作用最后再說
remove(eldest.key);
}
// Create new entry, link it on to list, and put it into table
LinkedEntry<K, V> oldTail = header.prv;
LinkedEntry<K, V> newTail = new LinkedEntry<K,V>(
key, value, hash, table[index], header, oldTail);
table[index] = oldTail.nxt = header.prv = newTail;
}
- 如果當前雙鏈表中有該元素,則將該元素移出鏈表,再重新將該元素接到雙鏈表尾端
- 如果雙鏈表中沒有對應元素,則創建一個新的 LinkedEntry ,其前一節點指向鏈表末端,其下節點指向 header,將其插入到鏈表末端,并將原來末端的下一節點指向新元素,將 header 的前一節點指向新元素,并將新元素的 next 執行原來哈希數組中該位置元素,并為哈希數組中對應位置賦值為新元素
get(Object key)
由 HashMap 中遍歷數組,改為了遍歷雙鏈表,效率更高。
需要注意的是如果 LinkedHashMap 的 accessOrder 為 true 時,會將需要獲取的元素移出雙鏈表,并重新連接到鏈表的尾端。
@Override public V get(Object key) {
/*
* This method is overridden to eliminate the need for a polymorphic
* invocation in superclass at the expense of code duplication.
*/
if (key == null) {
HashMapEntry<K, V> e = entryForNullKey;
if (e == null)
return null;
if (accessOrder)
makeTail((LinkedEntry<K, V>) e);
return e.value;
}
int hash = Collections.secondaryHash(key);
HashMapEntry<K, V>[] tab = table;
for (HashMapEntry<K, V> e = tab[hash & (tab.length - 1)];
e != null; e = e.next) {
K eKey = e.key;
if (eKey == key || (e.hash == hash && key.equals(eKey))) {
if (accessOrder)
makeTail((LinkedEntry<K, V>) e);
return e.value;
}
}
return null;
}
remove(Object key)
LinkedHashMap 重寫了 HashMap 的 postRemove 方法,HashMap 的 remove 方法中會將哈希數組中的元素移除,同時還會調用 postRemove 方法,該方法在 HashMap 中是空實現,在 LinkedHashMap 中實現如下:
@Override void postRemove(HashMapEntry<K, V> e) {
LinkedEntry<K, V> le = (LinkedEntry<K, V>) e;
le.prv.nxt = le.nxt;
le.nxt.prv = le.prv;
le.nxt = le.prv = null; // Help the GC (for performance)
}
postRemove 方法中,會將對應元素在雙鏈表中刪除
LinkedHashMap 實現 LRU 算法
剛才分析 addNewEntry 時提到了 removeEldestEntry 方法,其在 LinkedHashMap 中是個空實現
@Override void addNewEntry(K key, V value, int hash, int index) {
...
LinkedEntry<K, V> eldest = header.nxt;
if (eldest != header && removeEldestEntry(eldest)) {
remove(eldest.key);
}
...
}
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
return false;
}
在每次添加元素是都會調用 removeEldestEntry 方法,如果該方法返回 true 則刪除 header.nxt 處的元素,其實也就是刪除哈希數組中對應元素與雙向鏈表的頭部的元素,因為在 accessOrder 為 true 時每次插入和訪問都會將最近訪問的元素移動到雙向鏈表的尾部,這樣鏈表頭部的元素就是最久沒有被訪問到的。在 removeEldestEntry 中可以根據當前鏈表節點數到達最大容量時返回 true,此時就會刪除鏈表頭部節點,這樣就完成了 LRU 算法。
總結
- 實際上就是HashMap和LinkedList兩個集合類的存儲結構的結合。在LinkedHashMapMap中,所有put進來的Entry都保存在如第一個圖所示的哈希表中,但它又額外定義了一個以head為頭結點的空的雙向循環鏈表,每次put進來Entry,除了將其保存到對哈希表中對應的位置上外,還要將其插入到雙向循環鏈表的尾部。
2、LinkedHashMap由于繼承自HashMap,因此它具有HashMap的所有特性,同樣允許key和value為null。
注意構造方法,前四個構造方法都將accessOrder設為false,說明默認是按照插入順序排序的,而第五個構造方法可以自定義傳入的accessOrder的值,因此可以指定雙向循環鏈表中元素的排序規則,一般要用LinkedHashMap實現LRU算法,就要用該構造方法,將accessOrder置為true。
最后說說LinkedHashMap是如何實現LRU的。首先,當accessOrder為true時,才會開啟按訪問順序排序的模式,才能用來實現LRU算法。我們可以看到,無論是put方法還是get方法,都會導致目標Entry成為最近訪問的Entry,因此便把該Entry加入到了雙向鏈表的末尾(get方法通過調用recordAccess方法來實現,put方法在覆蓋已有key的情況下,也是通過調用recordAccess方法來實現,在插入新的Entry時,則是通過createEntry中的addBefore方法來實現),這樣便把最近使用了的Entry放入到了雙向鏈表的后面,多次操作后,雙向鏈表前面的Entry便是最近沒有使用的,這樣當節點個數滿的時候,刪除的最前面的Entry(head后面的那個Entry)便是最近最少使用的Entry。
六、TreeMap
TreeMap是基于紅黑樹實現的,這里只對紅黑樹做個簡單的介紹,紅黑樹是一種特殊的二叉排序樹,紅黑樹通過一些限制,使其不會出現二叉樹排序樹中極端的一邊倒的情況,相對二叉排序樹而言,這自然提高了查詢的效率。
紅黑樹規則
- 每個節點都只能是紅色或者黑色
- 根節點是黑色
- 每個葉節點(NIL節點,空節點)是黑色的。
- 如果一個結點是紅的,則它兩個子節點都是黑的。也就是說在一條路徑上不能出現相鄰的兩個紅色結點。
- 從任一節點到其每個葉子的所有路徑都包含相同數目的黑色節點。
正是這些性質的限制,使得紅黑樹中任一節點到其子孫葉子節點的最長路徑不會長于最短路徑的2倍,因此它是一種接近平衡的二叉樹。
構造方法
采用無參構造方法,不指定比較器,這時候,排序的實現要依賴key.compareTo()方法,因此key必須實現Comparable接口,并覆寫其中的compareTo方法。
采用帶比較器的構造方法,這時候,排序依賴該比較器,key可以不用實現Comparable接口。
帶Map的構造方法,該構造方法同樣不指定比較器,調用putAll方法將Map中的所有元素加入到TreeMap中。putAll的源碼如下:
TreeMap是根據key進行排序的,它的排序和定位需要依賴比較器或覆寫Comparable接口,也因此不需要key覆寫hashCode方法和equals方法,就可以排除掉重復的key,而HashMap的key則需要通過覆寫hashCode方法和equals方法來確保沒有重復的key。
TreeMap的查詢、插入、刪除效率均沒有HashMap高,一般只有要對key排序時才使用TreeMap。
TreeMap的key不能為null,而HashMap的key可以為null。
七、HashSet
實現原理基于 HashMap set 中的 value 都一樣,key 為添加的元素
HashSet 中有一個 HashMap 成員,每次操作時都是操作該 HashMap,操作的元素的 key 為 Set 中要操作的元素,value 為 HashSet 本身的引用。
八、TreeSet
實現原理基于 TreeMap