一、List
1、ArrayList
- ArrayList是一種變長的集合類,基於定長數組實現,使用默認構造方法初始化出來的容量是10(1.7之後都是延遲初始化,即第一次調用add方法添加元素的時候纔將elementData容量初始化爲10)
- ArrayList允許空值和重複元素,當往ArrayList中添加的元素數量大於其底層數組容量時,其會通過擴容機制重新生成一個更大的數組。ArrayList擴容的長度是原長度的1.5倍
- 由於ArrayList底層基於數組實現,所以其可以保證在的時間複雜度下完成隨機查找操作
- ArrayList是非線程安全類
- 刪除和插入需要調用
System.arraycopy
方法複製數組,性能差
2、LinkedList
1)、特性
LinkedList進行節點插入、刪除時間複雜度是,但是隨機訪問時間複雜度是
2)、底層數據結構
LinkedList底層實現是一個雙向鏈表
private static class Node<E> {
//節點的值
E item;
//後繼節點
Node<E> next;
//前驅結點
Node<E> prev;
Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}
二、Set
HashSet、TreeSet、LinkedHashSet底層都是基於其相對應的Map實現的,只使用了Map的key,保證了Set中的元素不重複
三、Map
1、HashMap
HashMap是基於鍵的hashCode值唯一標識一條數據,同時基於鍵的hashCode值進行數據的存取,因此可以快速地更新和查詢數據,但其每次遍歷的順序無法保證相同。HashMap的key和value允許爲null
HashMap是非線程安全的,即在同一時刻有多個線程同時寫HashMap時將可能導致數據的不一致。如果需要滿足線程安全的條件,則可以用Collections.synchronizedMap
使HashMap具有線程安全的能力,或者使用ConcurrentHashMap
1)、底層數據結構
HashMap的數據結構如下圖所示,其內部是一個數組,數組中的每個元素都是一個單向鏈表,鏈表中的每個元素都是嵌套類Entry的實例,Entry實例包含4個屬性:key、value、hash值和用於指向單向鏈表下一個元素的next
鏈表主要是爲了解決數組中的key發生哈希衝突時,將發生碰撞的key存儲到鏈表中
當哈希衝突嚴重時,在桶上形成的鏈表會變得越來越長,這樣在查詢時的效率就會越來越低,時間複雜度爲
所以在JDK1.8中,當鏈表長度大於8且HashMap數組長度大於等於64(數組長度小於64進行擴容操作)時,會將鏈表轉換爲紅黑樹,修改爲紅黑樹之後查詢效率變爲了
HashMap不直接使用紅黑樹,是因爲樹節點所佔空間是普通節點的兩倍,所以只有當節點足夠的時候,纔會使用樹節點。也就是說,儘管時間複雜度上,紅黑樹比鏈表好一點,但是紅黑樹所佔的空間比較大,所以綜合考慮之下,只有在鏈表節點數太多的時候,紅黑樹佔空間大這一劣勢不太明顯的時候,纔會捨棄鏈表,使用紅黑樹
2)、put方法流程圖
3)、默認初始化大小是多少?HashMap的擴容方式?負載因子是多少?
//默認容量
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
//默認的負載因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//用於判斷是否需要將鏈表轉換爲紅黑樹的閾值
static final int TREEIFY_THRESHOLD = 8;
//JDK1.7中的HashEntry修改爲Node
transient Node<K,V>[] table;
//HashMap中存放KV的數量
transient int size;
//當HashMap的size大於threshold時會執行resize操作,threshold=capacity*loadFactor
int threshold;
//負載因子
final float loadFactor;
...
給定的默認容量爲16,負載因子爲0.75。Map在使用過程中不斷地往裏面存放數據,當數量達到了就需要將當前16的容量進行擴容
擴容過程分爲兩步:
- 擴容:創建一個新的Entry空數組,長度是原數組的2倍
- ReHash:遍歷原Entry數組,把所有的Entry重新Hash到新數組
長度擴大以後,Hash的規則也隨之改變,所以要進行ReHash操作
index = (length - 1) & hash(key)
負載因子需要在時間和空間成本上尋求一種折衷
負載因子越大,填滿的元素越多,空間利用率越高,但發生衝突的機會變大了
負載因子越小,填滿的元素越少,衝突發生的機會減小,但空間浪費了更多了,而且還會提高擴容rehash操作的次數
所以,選擇0.75作爲默認的負載因子,完全是時間和空間成本上尋求的一種折衷選擇
4)、爲什麼容量總是爲2的n次冪?這樣設計的目的是什麼?
HashMap的tableSizeFor()
方法做了處理,能保證HashMap的容量永遠都是2的n次冪
因爲在使用2的冪的數字的時候,length-1的值是所有二進制位全爲1,這種情況下,index的結果等同於hashCode後幾位的值
只要輸入的hashCode本身分佈均勻,hash算法的結果就是均勻的,這樣設計的目的爲了實現均勻分佈
5)、線程不安全的原因
1)JDK1.7中擴容造成死循環分析過程
void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
for (Entry<K,V> e : table) {
while(null != e) {
Entry<K,V> next = e.next;
if (rehash) {
e.hash = null == e.key ? 0 : hash(e.key);
}
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;
}
}
}
在對table進行擴容到newTable後,需要將原來數據轉移到newTable中,JDK1.7在轉移元素的過程中,使用的是頭插法,也就是鏈表的順序會翻轉
假設:
- hash算法爲簡單的用key mod鏈表的大小
- 最開始hash表size=2,key=3、7、5,則都在table[1]中
- 然後進行resize,使size變成4
resize前狀態如下:
如果在單線程環境下,最後的結果如下:
在多線程環境下,假設有兩個線程A和B都在進行put操作。線程A在執行到transfer函數中代碼(1)處掛起
void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
for (Entry<K,V> e : table) {
while(null != e) {
Entry<K,V> next = e.next;
if (rehash) {
e.hash = null == e.key ? 0 : hash(e.key);
}
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;//(1)
e = next;
}
}
}
此時線程A中運行結果如下:
線程A掛起後,此時線程B正常執行,並完成resize操作,結果如下:
由於線程B已經執行完畢,根據Java內存模型,現在newTable和table中的Entry都是主存中最新值:7.next=3,3.next=null
此時切換到線程A上,在線程A掛起時內存中值如下:e=3,next=7,newTable[3]=null,代碼執行過程如下:
newTable[i] = e;//newTable[3] = 3
e = next;//e = 7
繼續循環:
e = 7;
Entry<K,V> next = e.next;//next = 3
e.next = newTable[i];//e.next = 3
newTable[i] = e;//newTable[3] = 7
e = next;//e = 3
再次循環:
e = 3;
Entry<K,V> next = e.next;//next = null
e.next = newTable[i];//3.next = 7
newTable[i] = e;//newTable[3] = 3
e = next;//e = null
e.next=7,而在上次循環中7.next=3,出現環形鏈表,並且此時e=null循環結束
2)JDK1.7中擴容造成數據丟失分析過程
線程A和線程B進行put操作,同樣線程A掛起:
void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
for (Entry<K,V> e : table) {
while(null != e) {
Entry<K,V> next = e.next;
if (rehash) {
e.hash = null == e.key ? 0 : hash(e.key);
}
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;//(1)
e = next;
}
}
}
此時線程A的運行結果如下:
此時線程B已獲得CPU時間片,並完成resize操作:
此時切換到線程A,在線程A掛起時:e=7,next=5,newTable[3]=null
執行newtable[i]=e,就將7放在了table[3]的位置,此時next=5。接着進行下一次循環:
e = 5;
Entry<K,V> next = e.next;//next = null
e.next = newTable[i];//e.next = 5
newTable[i] = e;//newTable[1] = 5
e = next;//e = null
將5放置在table[1]位置,此時e=null循環結束,3元素丟失,並形成環形鏈表
3)JDK1.8中的HashMap
在JDK1.8中對HashMap進行了優化,在發生哈希碰撞,不再採用頭插法方式,而是直接插入鏈表尾部,因此不會出現環形鏈表的情況
HashMap的put方法,如果沒有哈希碰撞則會直接插入元素。如果線程A和線程B同時進行put操作,剛好這兩條不同的數據hash值一樣,並且該位置數據爲null。假設一種情況,線程A進入後還未進行數據插入時掛起,而線程B正常執行,從而正常插入數據,然後線程A獲取CPU時間片,此時線程A不用再進行hash判斷了,這樣線程A會把線程B插入的數據給覆蓋,發生線程不安全
6)、哈希衝突解決方法
1)開放尋址法
開放尋址法的核心思想:如果出現了哈希衝突,就重新探測一個空閒位置,將其插入
線性探測插入操作:當往哈希表中插入數據時,如果某個數據經過哈希函數計算之後,存儲位置已經被佔用了,就從當前位置開始,依次往後查找,看是否有空閒位置,直到找到爲止
下圖中黃色的色塊表示空閒位置,橙色的色塊表示已經存儲了數據
當哈希表中插入的數據越來越多時,哈希衝突發生的可能性就會越來越大,空閒位置會越來越少,線性探測的時間就會越來越久
2)鏈表法
在哈希表中,每個桶或者槽會對應一條鏈表,所有哈希值相同的元素都放到相同槽位對應的鏈表中
7)、爲什麼要重寫hashcode和equals方法
class Key {
private Integer id;
public Integer getId() {
return id;
}
public Key(Integer id) {
this.id = id;
}
}
public class WithoutHashCode {
public static void main(String[] args) {
Key k1 = new Key(1);
Key k2 = new Key(1);
HashMap<Key, String> hashMap = new HashMap<>();
hashMap.put(k1, "Key with id is 1");
System.out.println(hashMap.get(k2));//null
}
}
當向HashMap中存入k1的時候,首先會調用Key這個類的hashcode方法,計算它的hash值,隨後把k1放入hash值所指引的內存位置,在Key這個類中沒有定義hashcode方法,就會調用Object類的hashcode方法,而Object類的hashcode方法返回的hash值是對象的地址。這時用k2去拿也會計算k2的hash值到相應的位置去拿,由於k1和k2的內存地址是不一樣的,所以用k2拿不到k1的值
重寫hashcode方法僅僅能夠k1和k2計算得到的hash值相同,調用get方法的時候會到正確的位置去找,但當出現哈希衝突時,在同一個位置有可能用鏈表的形式存放衝突元素,這時候就需要用到equals方法去對比了,由於沒有重寫equals方法,它會調用Object類的equals方法,Object的equals方法判斷的是兩個對象的內存地址是不是一樣,由於k1和k2都是new出來的,k1和k2的內存地址不相同,所以這時候用k2還是達不到k1的值
什麼時候需要重寫hashcode和equals方法?
在HashMap中存放自定義的鍵時,就需要重寫自定義對象的hashcode和equals方法
2、ConcurrentHashMap
1)、JDK1.7
JDK1.7中ConcurrentHashMap的鎖分段技術可有效提高併發訪問率:ConcurrentHashMap所使用的鎖分段技術,首先將數據分成一段一段地存儲,然後給每一段數據配一把鎖,當一個線程佔用鎖訪問其中一個段數據的時候,其他段的數據也能被其他線程訪問
ConcurrentHashMap是由Segment數組結構和HashEntry數組結構組成。Segment是一種可重入鎖,在ConcurrentHashMap裏扮演着鎖的角色;HashEntry則用於存儲鍵值對數據。一個ConcurrentHashMap裏包含一個Segment數組。Segment的結構和HashMap類似,是一種數組和鏈表結構。一個Segment裏包含一個HashEntry數組,每個HashEntry是一個鏈表結構的元素,每個Segment守護着一個HashEntry數組裏的元素,當對HashEntry數組的數據進行修改時,必須首先獲得與它對應的Segment鎖
ConcurrentHashMap重要的成員變量:
//Segment數組,存放數據時首先需要定位到具體的Segment中
final Segment<K,V>[] segments;
transient Set<K> keySet;
transient Set<Map.Entry<K,V>> entrySet;
Segment是ConcurrentHashMap的一個內部類,主要的組成如下:
static final class Segment<K,V> extends ReentrantLock implements Serializable {
private static final long serialVersionUID = 2249069246763182397L;
static final int MAX_SCAN_RETRIES =
Runtime.getRuntime().availableProcessors() > 1 ? 64 : 1;
//和HashMap中的HashEntry作用一樣,真正存放數據的桶
transient volatile HashEntry<K,V>[] table;
transient int count;
transient int modCount;
transient int threshold;
final float loadFactor;
HashEntry的組成:
static final class HashEntry<K,V> {
final int hash;
final K key;
volatile V value;
volatile HashEntry<K,V> next;
HashEntry(int hash, K key, V value, HashEntry<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
和HashMap非常類似,唯一的區別就是其中的核心數據如value,以及鏈表都是volatile修飾的,保證了獲取時的可見性
ConcurrentHashMap採用了分段鎖技術,其中Segment繼承於ReentrantLock。不會像HashTable那樣不管是put還是get操作都需要做同步處理,理論上ConcurrentHashMap支持CurrencyLevel(Segment數組數量)的線程併發。每當一個線程佔用鎖訪問一個Segment時,不會影響到其他的Segment
1)put方法
public V put(K key, V value) {
Segment<K,V> s;
if (value == null)
throw new NullPointerException();
int hash = hash(key);
int j = (hash >>> segmentShift) & segmentMask;
if ((s = (Segment<K,V>)UNSAFE.getObject
(segments, (j << SSHIFT) + SBASE)) == null)
s = ensureSegment(j);
return s.put(key, hash, value, false);
}
首先通過key定位到Segment,之後在對應的Segment中進行具體的put
final V put(K key, int hash, V value, boolean onlyIfAbsent) {
HashEntry<K,V> node = tryLock() ? null :
scanAndLockForPut(key, hash, value);
V oldValue;
try {
HashEntry<K,V>[] tab = table;
int index = (tab.length - 1) & hash;
HashEntry<K,V> first = entryAt(tab, index);
for (HashEntry<K,V> e = first;;) {
if (e != null) {
K k;
if ((k = e.key) == key ||
(e.hash == hash && key.equals(k))) {
oldValue = e.value;
if (!onlyIfAbsent) {
e.value = value;
++modCount;
}
break;
}
e = e.next;
}
else {
if (node != null)
node.setNext(first);
else
node = new HashEntry<K,V>(hash, key, value, first);
int c = count + 1;
if (c > threshold && tab.length < MAXIMUM_CAPACITY)
rehash(node);
else
setEntryAt(tab, index, node);
++modCount;
count = c;
oldValue = null;
break;
}
}
} finally {
unlock();
}
return oldValue;
}
雖然HashEntry中的value是用volatile關鍵詞修飾的,但是並不能保證併發的原子性,所以put操作時仍然需要加鎖處理
首先第一步的時候會嘗試獲取鎖,如果獲取失敗肯定就有其他線程存在競爭,則利用scanAndLockForPut()自旋獲取鎖
private HashEntry<K,V> scanAndLockForPut(K key, int hash, V value) {
HashEntry<K,V> first = entryForHash(this, hash);
HashEntry<K,V> e = first;
HashEntry<K,V> node = null;
int retries = -1;
//嘗試自旋獲取鎖
while (!tryLock()) {
HashEntry<K,V> f;
if (retries < 0) {
if (e == null) {
if (node == null)
node = new HashEntry<K,V>(hash, key, value, null);
retries = 0;
}
else if (key.equals(e.key))
retries = 0;
else
e = e.next;
}
//如果重試的次數達到了MAX_SCAN_RETRIES則改爲阻塞鎖獲取,保證能獲取成功
else if (++retries > MAX_SCAN_RETRIES) {
lock();
break;
}
else if ((retries & 1) == 0 &&
(f = entryForHash(this, hash)) != first) {
e = first = f;
retries = -1;
}
}
return node;
}
再結合起來看一下put的邏輯:
final V put(K key, int hash, V value, boolean onlyIfAbsent) {
//將當前Segment中的table通過key的hashcode定位到HashEntry
HashEntry<K,V> node = tryLock() ? null :
scanAndLockForPut(key, hash, value);
V oldValue;
try {
HashEntry<K,V>[] tab = table;
int index = (tab.length - 1) & hash;
HashEntry<K,V> first = entryAt(tab, index);
for (HashEntry<K,V> e = first;;) {
if (e != null) {
K k;
//遍歷該HashEntry,如果不爲空則判斷傳入的key和當前遍歷的key是否相等,相等則覆蓋舊的value
if ((k = e.key) == key ||
(e.hash == hash && key.equals(k))) {
oldValue = e.value;
if (!onlyIfAbsent) {
e.value = value;
++modCount;
}
break;
}
e = e.next;
}
else {
if (node != null)
node.setNext(first);
//爲空則需要新建一個HashEntry並加入到Segment中,同時會先判斷是否需要庫容弄
else
node = new HashEntry<K,V>(hash, key, value, first);
int c = count + 1;
if (c > threshold && tab.length < MAXIMUM_CAPACITY)
rehash(node);
else
setEntryAt(tab, index, node);
++modCount;
count = c;
oldValue = null;
break;
}
}
} finally {
//最後解除在scanAndLockForPut()方法中獲取的鎖
unlock();
}
return oldValue;
}
2)get方法
public V get(Object key) {
Segment<K,V> s;
HashEntry<K,V>[] tab;
int h = hash(key);
long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE;
if ((s = (Segment<K,V>)UNSAFE.getObjectVolatile(segments, u)) != null &&
(tab = s.table) != null) {
for (HashEntry<K,V> e = (HashEntry<K,V>) UNSAFE.getObjectVolatile
(tab, ((long)(((tab.length - 1) & h)) << TSHIFT) + TBASE);
e != null; e = e.next) {
K k;
if ((k = e.key) == key || (e.hash == h && key.equals(k)))
return e.value;
}
}
return null;
}
只需要將key通過hash之後定位到具體的Segment,再通過一次hash定位到具體的元素上
由於HashEntry中的value屬性是volatile關鍵詞修飾的,保證了內存可見性,所以每次獲取時都是最新值
ConcurrentHashMap的get方法是非常高效的,因爲整個過程都不需要加鎖
2)、JDK1.8
JDK1.8中拋棄了原有的Segment分段鎖,而採用了volatile+CAS+synchronized來保證併發安全性
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
volatile V val;
volatile Node<K,V> next;
Node(int hash, K key, V val, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.val = val;
this.next = next;
}
public final K getKey() { return key; }
public final V getValue() { return val; }
public final int hashCode() { return key.hashCode() ^ val.hashCode(); }
public final String toString(){ return key + "=" + val; }
public final V setValue(V value) {
throw new UnsupportedOperationException();
}
也將1.7中存放數據的HashEntry改爲了Node,但作用都是相同的
其中的val和next都用了volatile修飾,保證了可見性
1)put方法
public V put(K key, V value) {
return putVal(key, value, false);
}
final V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null) throw new NullPointerException();
//根據key計算出hashcode
int hash = spread(key.hashCode());
int binCount = 0;
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
//判斷是否需要進行初始化
if (tab == null || (n = tab.length) == 0)
tab = initTable();
//f爲當前key定位出的Node,如果爲空表示當前位置可以寫入數據,利用CAS嘗試寫入,失敗則自旋保證成功
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
break;
}
//如果當前位置的hashcode==MOVED==-1,則需要進行擴容
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
else {
V oldVal = null;
//如果都不滿足,則利用synchronized鎖寫入數據
synchronized (f) {
if (tabAt(tab, i) == f) {
if (fh >= 0) {
binCount = 1;
for (Node<K,V> e = f;; ++binCount) {
K ek;
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
oldVal = e.val;
if (!onlyIfAbsent)
e.val = value;
break;
}
Node<K,V> pred = e;
if ((e = e.next) == null) {
pred.next = new Node<K,V>(hash, key,
value, null);
break;
}
}
}
else if (f instanceof TreeBin) {
Node<K,V> p;
binCount = 2;
if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
value)) != null) {
oldVal = p.val;
if (!onlyIfAbsent)
p.val = value;
}
}
}
}
if (binCount != 0) {
//如果數量大於TREEIFY_THRESHOLD則要轉換爲紅黑樹
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
addCount(1L, binCount);
return null;
}
2)get方法
public V get(Object key) {
Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
int h = spread(key.hashCode());
if ((tab = table) != null && (n = tab.length) > 0 &&
(e = tabAt(tab, (n - 1) & h)) != null) {
if ((eh = e.hash) == h) {
if ((ek = e.key) == key || (ek != null && key.equals(ek)))
return e.val;
}
else if (eh < 0)
return (p = e.find(h, key)) != null ? p.val : null;
while ((e = e.next) != null) {
if (e.hash == h &&
((ek = e.key) == key || (ek != null && key.equals(ek))))
return e.val;
}
}
return null;
}
根據計算出來的hashcode尋址,如果就在桶上那麼直接返回值。如果是紅黑樹那就按照樹的方式獲取值。都不滿足那就按照鏈表的方式遍歷獲取值
JDK1.8在1.7的數據結構上做了大的改動,採用紅黑樹之後可以保證查詢效率,甚至取消了ReentrantLock改爲了synchronized,這樣可以看出在新版的JDK中對synchronized優化是很到位的
3)、ConcurrentHashMap/Hashtable不允許空值的原因
主要是因爲會產生歧義,如果支持空值,在使用map.get(key)
時,返回值爲null,可能有兩種情況:該key映射的值爲null,或者該key未映射到。如果是非併發映射中,可以使用map.contains(key)
進行檢查,但是在併發的情況下,兩次調用之間的映射可能已經更改了
3、HashMap和Hashtable的區別
1)線程安全
Hashtable是線程安全的,HashMap不是線程安全的
Hashtable所有的元素操作都是synchronized修飾的,而HashMap並沒有
2)性能優劣
Hashtable是線程安全的,每個方法都要阻塞其他線程,所以Hashtable性能較差,HashMap性能較好,使用更廣
如果要線程安全又要保證性能,建議使用JUC包下的ConcurrentHashMap
3)NULL
Hashtable是不允許鍵或值爲null的,HashMap的鍵值則都可以爲null
Hashtable代碼片段
public synchronized V put(K key, V value) {
//如果value爲null,直接拋出空指針異常
if (value == null) {
throw new NullPointerException();
}
Entry<?,?> tab[] = table;
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length;
@SuppressWarnings("unchecked")
Entry<K,V> entry = (Entry<K,V>)tab[index];
for(; entry != null ; entry = entry.next) {
if ((entry.hash == hash) && entry.key.equals(key)) {
V old = entry.value;
entry.value = value;
return old;
}
}
addEntry(hash, key, value, index);
return null;
}
HashMap代碼片段
static final int hash(Object key) {
int h;
//key爲null做了特殊處理
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
4)實現方式
Hashtable繼承源碼
public class Hashtable<K,V>
extends Dictionary<K,V>
implements Map<K,V>, Cloneable, java.io.Serializable {
HashMap繼承源碼
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable {
Hashtable繼承了Dictionary,而HashMap繼承了HashMap
5)容量擴容
HashMap的初始容量爲16,Hashtable初始容量爲11,兩者的負載因子默認都是0.75
Hashtable代碼片段
public Hashtable() {
this(11, 0.75f);
}
HashMap代碼片段
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
static final float DEFAULT_LOAD_FACTOR = 0.75f;
當現有容量大於總容量*負載因子時,HashMap擴容規則爲當前容量翻倍,Hashtable擴容規則爲當前容量翻倍+1
4、TreeMap
TreeMap基於紅黑樹實現。映射根據其鍵的自然順序進行排序,或者根據創建映射時提供的Comparator進行排序,具體取決於使用的構造方法
TreeMap因爲需要排序,進行key的compareTo()
方法,所以key是不能null值,value是可以的
5、LinkedHashMap
LinkedHashMap直接繼承自HashMap,這也就說明了HashMap一切重要的概念LinkedHashMap都是擁有的,這就包括了,hash算法定位hash桶位置,哈希表由數組和單鏈表構成,並且當單鏈表長度超過8的時候轉化爲紅黑樹,擴容體系,這一切都跟HashMap一樣。除此之外,LinkedHashMap比HashMap更加強大,這體現在:
- LinkedHashMap內部維護了一個雙向鏈表,解決了HashMap不能隨時保持遍歷順序和插入順序一致的問題
- LinkedHashMap元素的訪問順序也提供了相關支持,也就是常說的LRU(最近最少使用)原則
圖片中紅黃箭頭代表元素添加順序,藍箭頭代表單鏈表各個元素的存儲順序。head表示雙向鏈表頭部,tail代表雙向鏈表尾部
LinkedHashMap和HashMap相比,唯一的變化是使用雙向鏈表(圖中紅黃箭頭部分)記錄了元素的添加順序,HashMap的Node節點只有next指針,LinkedHashMap對於Node節點進行了擴展:
static class Entry<K,V> extends HashMap.Node<K,V> {
Entry<K,V> before, after;
Entry(int hash, K key, V value, Node<K,V> next) {
super(hash, key, value, next);
}
}
LinkedHashMap基本存儲單元Entry繼承自HashMap.Node
,並在此基礎上添加了before和after這兩個指針變量。before變量在每次添加元素的時候將會指向上一次添加的元素,而上一次添加元素的after變量將指向該次添加的元素,來形成雙向鏈接
LinkedHashMap支持兩種訪問訪問順序,這主要取決於accessOrder這個參數的值,當accessOrder爲false時按照插入順序訪問(默認),當accessOrder爲true時按照LRU Cache的機制進行訪問
//initialCapacity:初始化容量 loadFactor:負載因子 accessOrder:訪問順序(true代表使用LRU/false代表使用插入的順序)
public LinkedHashMap(int initialCapacity,
float loadFactor,
boolean accessOrder) {
super(initialCapacity, loadFactor);
this.accessOrder = accessOrder;
}
當某個位置的數據被命中,通過調整該數據的位置,將其移動至尾部。新插入的元素也是直接放入尾部(尾插法)。這樣一來,最近被命中的元素就向尾部移動,那麼鏈表的頭部就是最近最少使用的元素所在的位置
LinkedHashMap中並沒有覆寫任何關於HashMap的put方法,所以調用LinkedHashMap的put方法實際上調用了父類HashMap的方法
HashMap中put方法源碼如下:
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
//判斷當前桶是否爲空,空的就需要初始化(resize中會判斷是否需要初始化)
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//根據當前key的hashcode定位到具體的桶中並判斷是否爲空,爲空表明沒有Hash衝突就直接在當前位置創建一個新桶即可
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
//如果當前桶有值(Hash衝突),那麼就要比較當前桶中的key、key的hashcode與寫入的key是否相等,相等就賦值給e,後面統一進行賦值及返回
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
//如果當前桶爲紅黑樹,按照紅黑樹的方式寫入數據
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
//如果是個鏈表,就需要將當前的key、value封裝成一個新節點寫入當前桶的後面(採用尾插法)
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
//判斷當前鏈表的大小是否大於預設的閾值,大於時就要轉換爲紅黑樹
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
//如果在遍歷鏈表的過程中,找到key相同時直接退出遍歷
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
//如果e!=null就相當於存在相同的key,那就需要將值覆蓋
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
//最後判斷是否需要進行擴容
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
在putVal方法中如果map中存在相同的key時,會調用void afterNodeAccess(Node<K,V> p)
方法,該方法在HashMap中是空實現,但是在LinkedHasMap中重寫了該方法實現了將被訪問節點移動到鏈表最後
//將被訪問節點移動到鏈表最後
void afterNodeAccess(Node<K,V> e) { // move node to last
LinkedHashMap.Entry<K,V> last;
//accessOrder爲true時才支持LRU Cache
if (accessOrder && (last = tail) != e) {
//三個臨時變量:p爲當前被訪問節點,b爲其前驅結點,a爲其後繼節點
LinkedHashMap.Entry<K,V> p =
(LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
//訪問節點的後驅節點置爲null
p.after = null;
//如果訪問節點的前驅爲null,則說明p=head,由於這時p要移動到鏈表最後,所以a設置爲head
if (b == null)
head = a;
//否則b的後繼設置爲a
else
b.after = a;
//如果p不爲尾節點,那麼將a的前驅設置爲b
if (a != null)
a.before = b;
else
last = b;
if (last == null)
head = p;
else {
p.before = last;
last.after = p;
}
//將p接在雙向鏈表的最後
tail = p;
++modCount;
}
}
舉個例子,比如該次操作訪問的是13這個節點,而14是其後驅,11是其前驅,且tail=14。在通過get訪問13節點後,13變成了tail節點,而14變成了其前驅節點,相應的14的前驅變成11,11的後驅變成了14,14的後驅變成了13
而在putVal方法的最後會調用一個void afterNodeInsertion(boolean evict)
方法,,該方法在HashMap中是空實現,但是在LinkedHasMap中重寫了該方法實現了刪除頭節點(最近最少使用的元素)
void afterNodeInsertion(boolean evict) { // possibly remove eldest
LinkedHashMap.Entry<K,V> first;
if (evict && (first = head) != null && removeEldestEntry(first)) {//(1)
K key = first.key;
removeNode(hash(key), key, null, false, true);
}
}
protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
return false;
}
代碼(1)處:evict在put方法調用putVal時傳參即爲true,所以當map不爲空且removeEldestEntry返回true時就會刪除頭節點,但是在LinkedHasMap中removeEldestEntry方法始終返回true,所以如果要基於LinkedHashMap實現LRU則需要重寫removeEldestEntry方法,當map的size大於初始化容量時返回true