HashMap 的實現原理
HashMap 概述
HashMap 是基於哈希表的 Map 接口的非同步實現。此實現提供所有可選的映射操作,並允許使用 null 值和 null 鍵。此類不保證映射的順序,特別是它不保證該順序恆久不變。
此實現假定哈希函數將元素適當地分布在各桶之間,可為基本操作(get 和 put)提供穩定的性能。迭代 collection 視圖所需的時間與 HashMap 實例的“容量”(桶的數量)及其大小(鍵-值映射關系數)成比例。所以,如果迭代性能很重要,則不要將初始容量設置得太高或將加載因子設置得太低。也許大家開始對這段話有一點不太懂,不過不用擔心,當你讀完這篇文章后,就能深切理解這其中的含義了。
需要注意的是:Hashmap 不是同步的,如果多個線程同時訪問一個 HashMap,而其中至少一個線程從結構上(指添加或者刪除一個或多個映射關系的任何操作)修改了,則必須保持外部同步,以防止對映射進行意外的非同步訪問。
HashMap 的數據結構
在 Java 編程語言中,最基本的結構就是兩種,一個是數組,另外一個是指針(引用),HashMap 就是通過這兩個數據結構進行實現。HashMap實際上是一個“鏈表散列”的數據結構,即數組和鏈表的結合體。
從上圖中可以看出,HashMap 底層就是一個數組結構,數組中的每一項又是一個鏈表。當新建一個 HashMap 的時候,就會初始化一個數組。
我們通過 JDK 中的 HashMap 源碼進行一些學習,首先看一下構造函數:
-
public HashMap(int initialCapacity, float loadFactor) {
-
if (initialCapacity < 0)
-
throw new IllegalArgumentException("Illegal initial capacity: " +
-
initialCapacity);
-
if (initialCapacity > MAXIMUM_CAPACITY)
-
initialCapacity = MAXIMUM_CAPACITY;
-
if (loadFactor <= 0 || Float.isNaN(loadFactor))
-
throw new IllegalArgumentException("Illegal load factor: " +
-
loadFactor);
-
-
// Find a power of 2 >= initialCapacity
-
int capacity = 1;
-
while (capacity < initialCapacity)
-
capacity <<= 1;
-
-
this.loadFactor = loadFactor;
-
threshold = ( int)Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
-
table = new Entry[capacity];
-
useAltHashing = sun.misc.VM.isBooted() &&
-
(capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
-
init();
-
}
我們着重看一下第 18 行代碼table = new Entry[capacity];。這不就是 Java 中數組的創建方式嗎?也就是說在構造函數中,其創建了一個 Entry 的數組,其大小為 capacity(目前我們還不需要太了解該變量含義),那么 Entry 又是什么結構呢?看一下源碼:
-
static class Entry<K,V> implements Map.Entry<K,V> {
-
final K key;
-
V value;
-
Entry<K,V> next;
-
final int hash;
-
……
-
}
我們目前還是只着重核心的部分,Entry 是一個 static class,其中包含了 key 和 value,也就是鍵值對,另外還包含了一個 next 的 Entry 指針。我們可以總結出:Entry 就是數組中的元素,每個 Entry 其實就是一個 key-value 對,它持有一個指向下一個元素的引用,這就構成了鏈表。
HashMap 的核心方法解讀
存儲
-
/**
-
* Associates the specified value with the specified key in this map.
-
* If the map previously contained a mapping for the key, the old
-
* value is replaced.
-
*
-
* @param key key with which the specified value is to be associated
-
* @param value value to be associated with the specified key
-
* @return the previous value associated with <tt>key</tt>, or
-
* <tt>null</tt> if there was no mapping for <tt>key</tt>.
-
* (A <tt>null</tt> return can also indicate that the map
-
* previously associated <tt>null</tt> with <tt>key</tt>.)
-
*/
-
public V put(K key, V value) {
-
//其允許存放null的key和null的value,當其key為null時,調用putForNullKey方法,放入到table[0]的這個位置
-
if (key == null)
-
return putForNullKey(value);
-
//通過調用hash方法對key進行哈希,得到哈希之后的數值。該方法實現可以通過看源碼,其目的是為了盡可能的讓鍵值對可以分不到不同的桶中
-
int hash = hash(key);
-
//根據上一步驟中求出的hash得到在數組中是索引i
-
int i = indexFor(hash, table.length);
-
//如果i處的Entry不為null,則通過其next指針不斷遍歷e元素的下一個元素。
-
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
-
Object k;
-
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
-
V oldValue = e.value;
-
e.value = value;
-
e.recordAccess( this);
-
return oldValue;
-
}
-
}
-
-
modCount++;
-
addEntry(hash, key, value, i);
-
return null;
-
}
我們看一下方法的標准注釋:在注釋中首先提到了,當我們 put 的時候,如果 key 存在了,那么新的 value 會代替舊的 value,並且如果 key 存在的情況下,該方法返回的是舊的 value,如果 key 不存在,那么返回 null。
從上面的源代碼中可以看出:當我們往 HashMap 中 put 元素的時候,先根據 key 的 hashCode 重新計算 hash 值,根據 hash 值得到這個元素在數組中的位置(即下標),如果數組該位置上已經存放有其他元素了,那么在這個位置上的元素將以鏈表的形式存放,新加入的放在鏈頭,最先加入的放在鏈尾。如果數組該位置上沒有元素,就直接將該元素放到此數組中的該位置上。
addEntry(hash, key, value, i)方法根據計算出的 hash 值,將 key-value 對放在數組 table 的 i 索引處。addEntry 是 HashMap 提供的一個包訪問權限的方法,代碼如下:
-
/**
-
* Adds a new entry with the specified key, value and hash code to
-
* the specified bucket. It is the responsibility of this
-
* method to resize the table if appropriate.
-
*
-
* Subclass overrides this to alter the behavior of put method.
-
*/
-
void addEntry(int hash, K key, V value, int bucketIndex) {
-
if ((size >= threshold) && (null != table[bucketIndex])) {
-
resize( 2 * table.length);
-
hash = ( null != key) ? hash(key) : 0;
-
bucketIndex = indexFor(hash, table.length);
-
}
-
-
createEntry(hash, key, value, bucketIndex);
-
}
-
void createEntry(int hash, K key, V value, int bucketIndex) {
-
// 獲取指定 bucketIndex 索引處的 Entry
-
Entry<K,V> e = table[bucketIndex];
-
// 將新創建的 Entry 放入 bucketIndex 索引處,並讓新的 Entry 指向原來的 Entr
-
table[bucketIndex] = new Entry<>(hash, key, value, e);
-
size++;
-
}
當系統決定存儲 HashMap 中的 key-value 對時,完全沒有考慮 Entry 中的 value,僅僅只是根據 key 來計算並決定每個 Entry 的存儲位置。我們完全可以把 Map 集合中的 value 當成 key 的附屬,當系統決定了 key 的存儲位置之后,value 隨之保存在那里即可。
hash(int h)方法根據 key 的 hashCode 重新計算一次散列。此算法加入了高位計算,防止低位不變,高位變化時,造成的 hash 沖突。
-
final int hash(Object k) {
-
int h = 0;
-
if (useAltHashing) {
-
if (k instanceof String) {
-
return sun.misc.Hashing.stringHash32((String) k);
-
}
-
h = hashSeed;
-
}
-
//得到k的hashcode值
-
h ^= k.hashCode();
-
//進行計算
-
h ^= (h >>> 20) ^ (h >>> 12);
-
return h ^ (h >>> 7) ^ (h >>> 4);
-
}
我們可以看到在 HashMap 中要找到某個元素,需要根據 key 的 hash 值來求得對應數組中的位置。如何計算這個位置就是 hash 算法。前面說過 HashMap 的數據結構是數組和鏈表的結合,所以我們當然希望這個 HashMap 里面的 元素位置盡量的分布均勻些,盡量使得每個位置上的元素數量只有一個,那么當我們用 hash 算法求得這個位置的時候,馬上就可以知道對應位置的元素就是我們要的,而不用再去遍歷鏈表,這樣就大大優化了查詢的效率。
對於任意給定的對象,只要它的 hashCode() 返回值相同,那么程序調用 hash(int h) 方法所計算得到的 hash 碼值總是相同的。我們首先想到的就是把 hash 值對數組長度取模運算,這樣一來,元素的分布相對來說是比較均勻的。但是,“模”運算的消耗還是比較大的,在 HashMap 中是這樣做的:調用 indexFor(int h, int length) 方法來計算該對象應該保存在 table 數組的哪個索引處。indexFor(int h, int length) 方法的代碼如下:
-
/**
-
* Returns index for hash code h.
-
*/
-
static int indexFor(int h, int length) {
-
return h & (length-1);
-
}
這個方法非常巧妙,它通過 h & (table.length -1) 來得到該對象的保存位,而 HashMap 底層數組的長度總是 2 的 n 次方,這是 HashMap 在速度上的優化。在 HashMap 構造器中有如下代碼:
-
// Find a power of 2 >= initialCapacity
-
int capacity = 1;
-
while (capacity < initialCapacity)
-
capacity <<= 1;
這段代碼保證初始化時 HashMap 的容量總是 2 的 n 次方,即底層數組的長度總是為 2 的 n 次方。
當 length 總是 2 的 n 次方時,h& (length-1)運算等價於對 length 取模,也就是 h%length,但是 & 比 % 具有更高的效率。這看上去很簡單,其實比較有玄機的,我們舉個例子來說明:
假設數組長度分別為 15 和 16,優化后的 hash 碼分別為 8 和 9,那么 & 運算后的結果如下:
| h & (table.length-1) | hash | table.length-1 | ||
|---|---|---|---|---|
| 8 & (15-1): | 0100 | & | 1110 | = 0100 |
| 9 & (15-1): | 0101 | & | 1110 | = 0100 |
| 8 & (16-1): | 0100 | & | 1111 | = 0100 |
| 9 & (16-1): | 0101 | & | 1111 | = 0101 |
從上面的例子中可以看出:當它們和 15-1(1110)“與”的時候,產生了相同的結果,也就是說它們會定位到數組中的同一個位置上去,這就產生了碰撞,8 和 9 會被放到數組中的同一個位置上形成鏈表,那么查詢的時候就需要遍歷這個鏈 表,得到8或者9,這樣就降低了查詢的效率。同時,我們也可以發現,當數組長度為 15 的時候,hash 值會與 15-1(1110)進行“與”,那么最后一位永遠是 0,而 0001,0011,0101,1001,1011,0111,1101 這幾個位置永遠都不能存放元素了,空間浪費相當大,更糟的是這種情況中,數組可以使用的位置比數組長度小了很多,這意味着進一步增加了碰撞的幾率,減慢了查詢的效率!而當數組長度為16時,即為2的n次方時,2n-1 得到的二進制數的每個位上的值都為 1,這使得在低位上&時,得到的和原 hash 的低位相同,加之 hash(int h)方法對 key 的 hashCode 的進一步優化,加入了高位計算,就使得只有相同的 hash 值的兩個值才會被放到數組中的同一個位置上形成鏈表。
所以說,當數組長度為 2 的 n 次冪的時候,不同的 key 算得得 index 相同的幾率較小,那么數據在數組上分布就比較均勻,也就是說碰撞的幾率小,相對的,查詢的時候就不用遍歷某個位置上的鏈表,這樣查詢效率也就較高了。
根據上面 put 方法的源代碼可以看出,當程序試圖將一個key-value對放入HashMap中時,程序首先根據該 key 的 hashCode() 返回值決定該 Entry 的存儲位置:如果兩個 Entry 的 key 的 hashCode() 返回值相同,那它們的存儲位置相同。如果這兩個 Entry 的 key 通過 equals 比較返回 true,新添加 Entry 的 value 將覆蓋集合中原有 Entry 的 value,但key不會覆蓋。如果這兩個 Entry 的 key 通過 equals 比較返回 false,新添加的 Entry 將與集合中原有 Entry 形成 Entry 鏈,而且新添加的 Entry 位於 Entry 鏈的頭部——具體說明繼續看 addEntry() 方法的說明。
讀取
-
/**
-
* Returns the value to which the specified key is mapped,
-
* or {@code null} if this map contains no mapping for the key.
-
*
-
* <p>More formally, if this map contains a mapping from a key
-
* {@code k} to a value {@code v} such that {@code (key==null ? k==null :
-
* key.equals(k))}, then this method returns {@code v}; otherwise
-
* it returns {@code null}. (There can be at most one such mapping.)
-
*
-
* <p>A return value of {@code null} does not <i>necessarily</i>
-
* indicate that the map contains no mapping for the key; it's also
-
* possible that the map explicitly maps the key to {@code null}.
-
* The {@link #containsKey containsKey} operation may be used to
-
* distinguish these two cases.
-
*
-
* @see #put(Object, Object)
-
*/
-
public V get(Object key) {
-
if (key == null)
-
return getForNullKey();
-
Entry<K,V> entry = getEntry(key);
-
-
return null == entry ? null : entry.getValue();
-
}
-
final Entry<K,V> getEntry(Object key) {
-
int hash = (key == null) ? 0 : hash(key);
-
for (Entry<K,V> e = table[indexFor(hash, table.length)];
-
e != null;
-
e = e.next) {
-
Object k;
-
if (e.hash == hash &&
-
((k = e.key) == key || (key != null && key.equals(k))))
-
return e;
-
}
-
return null;
-
}
有了上面存儲時的 hash 算法作為基礎,理解起來這段代碼就很容易了。從上面的源代碼中可以看出:從 HashMap 中 get 元素時,首先計算 key 的 hashCode,找到數組中對應位置的某一元素,然后通過 key 的 equals 方法在對應位置的鏈表中找到需要的元素。
歸納
簡單地說,HashMap 在底層將 key-value 當成一個整體進行處理,這個整體就是一個 Entry 對象。HashMap 底層采用一個 Entry[] 數組來保存所有的 key-value 對,當需要存儲一個 Entry 對象時,會根據 hash 算法來決定其在數組中的存儲位置,在根據 equals 方法決定其在該數組位置上的鏈表中的存儲位置;當需要取出一個Entry 時,也會根據 hash 算法找到其在數組中的存儲位置,再根據 equals 方法從該位置上的鏈表中取出該Entry。
HashMap 的 resize(rehash)
當 HashMap 中的元素越來越多的時候,hash 沖突的幾率也就越來越高,因為數組的長度是固定的。所以為了提高查詢的效率,就要對 HashMap 的數組進行擴容,數組擴容這個操作也會出現在 ArrayList 中,這是一個常用的操作,而在 HashMap 數組擴容之后,最消耗性能的點就出現了:原數組中的數據必須重新計算其在新數組中的位置,並放進去,這就是 resize。
那么 HashMap 什么時候進行擴容呢?當 HashMap 中的元素個數超過數組大小 *loadFactor時,就會進行數組擴容,loadFactor的默認值為 0.75,這是一個折中的取值。也就是說,默認情況下,數組大小為 16,那么當 HashMap 中元素個數超過 16*0.75=12 的時候,就把數組的大小擴展為 2*16=32,即擴大一倍,然后重新計算每個元素在數組中的位置,而這是一個非常消耗性能的操作,所以如果我們已經預知 HashMap 中元素的個數,那么預設元素的個數能夠有效的提高 HashMap 的性能。
HashMap 的性能參數
HashMap 包含如下幾個構造器:
- HashMap():構建一個初始容量為 16,負載因子為 0.75 的 HashMap。
- ashMap(int initialCapacity):構建一個初始容量為 initialCapacity,負載因子為 0.75 的 HashMap。
- HashMap(int initialCapacity, float loadFactor):以指定初始容量、指定的負載因子創建一個 HashMap。
HashMap 的基礎構造器 HashMap(int initialCapacity, float loadFactor) 帶有兩個參數,它們是初始容量 initialCapacity 和負載因子 loadFactor。
負載因子 loadFactor 衡量的是一個散列表的空間的使用程度,負載因子越大表示散列表的裝填程度越高,反之愈小。對於使用鏈表法的散列表來說,查找一個元素的平均時間是 O(1+a),因此如果負載因子越大,對空間的利用更充分,然而后果是查找效率的降低;如果負載因子太小,那么散列表的數據將過於稀疏,對空間造成嚴重浪費。
HashMap 的實現中,通過 threshold 字段來判斷 HashMap 的最大容量:
threshold = (int)(capacity * loadFactor);
結合負載因子的定義公式可知,threshold 就是在此 loadFactor 和 capacity 對應下允許的最大元素數目,超過這個數目就重新 resize,以降低實際的負載因子。默認的的負載因子 0.75 是對空間和時間效率的一個平衡選擇。當容量超出此最大容量時, resize 后的 HashMap 容量是容量的兩倍:
Fail-Fast 機制
原理
我們知道 java.util.HashMap 不是線程安全的,因此如果在使用迭代器的過程中有其他線程修改了 map,那么將拋出 ConcurrentModificationException,這就是所謂 fail-fast 策略。
ail-fast 機制是 java 集合(Collection)中的一種錯誤機制。 當多個線程對同一個集合的內容進行操作時,就可能會產生 fail-fast 事件。
例如:當某一個線程 A 通過 iterator去遍歷某集合的過程中,若該集合的內容被其他線程所改變了;那么線程 A 訪問集合時,就會拋出 ConcurrentModificationException 異常,產生 fail-fast 事件。
這一策略在源碼中的實現是通過 modCount 域,modCount 顧名思義就是修改次數,對 HashMap 內容(當然不僅僅是 HashMap 才會有,其他例如 ArrayList 也會)的修改都將增加這個值(大家可以再回頭看一下其源碼,在很多操作中都有 modCount++ 這句),那么在迭代器初始化過程中會將這個值賦給迭代器的 expectedModCount。
-
HashIterator() {
-
expectedModCount = modCount;
-
if (size > 0) { // advance to first entry
-
Entry[] t = table;
-
while (index < t.length && (next = t[index++]) == null)
-
;
-
}
-
}
在迭代過程中,判斷 modCount 跟 expectedModCount 是否相等,如果不相等就表示已經有其他線程修改了 Map:
注意到 modCount 聲明為 volatile,保證線程之間修改的可見性。
-
final Entry<K,V> nextEntry() {
-
if (modCount != expectedModCount)
-
throw new ConcurrentModificationException();
在 HashMap 的 API 中指出:
由所有 HashMap 類的“collection 視圖方法”所返回的迭代器都是快速失敗的:在迭代器創建之后,如果從結構上對映射進行修改,除非通過迭代器本身的 remove 方法,其他任何時間任何方式的修改,迭代器都將拋出 ConcurrentModificationException。因此,面對並發的修改,迭代器很快就會完全失敗,而不冒在將來不確定的時間發生任意不確定行為的風險。
注意,迭代器的快速失敗行為不能得到保證,一般來說,存在非同步的並發修改時,不可能作出任何堅決的保證。快速失敗迭代器盡最大努力拋出 ConcurrentModificationException。因此,編寫依賴於此異常的程序的做法是錯誤的,正確做法是:迭代器的快速失敗行為應該僅用於檢測程序錯誤。
解決方案
在上文中也提到,fail-fast 機制,是一種錯誤檢測機制。它只能被用來檢測錯誤,因為 JDK 並不保證 fail-fast 機制一定會發生。若在多線程環境下使用 fail-fast 機制的集合,建議使用“java.util.concurrent 包下的類”去取代“java.util 包下的類”。
HashMap 的兩種遍歷方式
第一種
-
Map map = new HashMap();
-
Iterator iter = map.entrySet().iterator();
-
while (iter.hasNext()) {
-
Map.Entry entry = (Map.Entry) iter.next();
-
Object key = entry.getKey();
-
Object val = entry.getValue();
-
}
效率高,以后一定要使用此種方式!
第二種
-
Map map = new HashMap();
-
Iterator iter = map.keySet().iterator();
-
while (iter.hasNext()) {
-
Object key = iter.next();
-
Object val = map.get(key);
-
}
效率低,以后盡量少使用!
HashSet 的實現原理
HashSet 概述
對於 HashSet 而言,它是基於 HashMap 實現的,底層采用 HashMap 來保存元素,所以如果對 HashMap 比較熟悉了,那么學習 HashSet 也是很輕松的。
我們先通過 HashSet 最簡單的構造函數和幾個成員變量來看一下,證明咱們上邊說的,其底層是 HashMap:
-
private transient HashMap<E,Object> map;
-
-
// Dummy value to associate with an Object in the backing Map
-
private static final Object PRESENT = new Object();
-
-
/**
-
* Constructs a new, empty set; the backing <tt>HashMap</tt> instance has
-
* default initial capacity (16) and load factor (0.75).
-
*/
-
public HashSet() {
-
map = new HashMap<>();
-
}
其實在英文注釋中已經說的比較明確了。首先有一個HashMap的成員變量,我們在 HashSet 的構造函數中將其初始化,默認情況下采用的是 initial capacity為16,load factor 為 0.75。
HashSet 的實現
對於 HashSet 而言,它是基於 HashMap 實現的,HashSet 底層使用 HashMap 來保存所有元素,因此 HashSet 的實現比較簡單,相關 HashSet 的操作,基本上都是直接調用底層 HashMap 的相關方法來完成,我們應該為保存到 HashSet 中的對象覆蓋 hashCode() 和 equals()
構造方法
-
/**
-
* 默認的無參構造器,構造一個空的HashSet。
-
*
-
* 實際底層會初始化一個空的HashMap,並使用默認初始容量為16和加載因子0.75。
-
*/
-
public HashSet() {
-
map = new HashMap<E,Object>();
-
}
-
-
/**
-
* 構造一個包含指定collection中的元素的新set。
-
*
-
* 實際底層使用默認的加載因子0.75和足以包含指定collection中所有元素的初始容量來創建一個HashMap。
-
* @param c 其中的元素將存放在此set中的collection。
-
*/
-
public HashSet(Collection<? extends E> c) {
-
map = new HashMap<E,Object>(Math.max((int) (c.size()/.75f) + 1, 16));
-
addAll(c);
-
}
-
-
/**
-
* 以指定的initialCapacity和loadFactor構造一個空的HashSet。
-
*
-
* 實際底層以相應的參數構造一個空的HashMap。
-
* @param initialCapacity 初始容量。
-
* @param loadFactor 加載因子。
-
*/
-
public HashSet(int initialCapacity, float loadFactor) {
-
map = new HashMap<E,Object>(initialCapacity, loadFactor);
-
}
-
-
/**
-
* 以指定的initialCapacity構造一個空的HashSet。
-
*
-
* 實際底層以相應的參數及加載因子loadFactor為0.75構造一個空的HashMap。
-
* @param initialCapacity 初始容量。
-
*/
-
public HashSet(int initialCapacity) {
-
map = new HashMap<E,Object>(initialCapacity);
-
}
-
-
/**
-
* 以指定的initialCapacity和loadFactor構造一個新的空鏈接哈希集合。此構造函數為包訪問權限,不對外公開,
-
* 實際只是是對LinkedHashSet的支持。
-
*
-
* 實際底層會以指定的參數構造一個空LinkedHashMap實例來實現。
-
* @param initialCapacity 初始容量。
-
* @param loadFactor 加載因子。
-
* @param dummy 標記。
-
*/
-
HashSet( int initialCapacity, float loadFactor, boolean dummy) {
-
map = new LinkedHashMap<E,Object>(initialCapacity, loadFactor);
-
}
add 方法
-
/**
-
-
* @param e 將添加到此set中的元素。
-
* @return 如果此set尚未包含指定元素,則返回true。
-
*/
-
public boolean add(E e) {
-
return map.put(e, PRESENT)==null;
-
}
如果此 set 中尚未包含指定元素,則添加指定元素。更確切地講,如果此 set 沒有包含滿足(e==null ? e2==null : e.equals(e2)) 的元素 e2,則向此 set 添加指定的元素 e。如果此 set 已包含該元素,則該調用不更改 set 並返回 false。但底層實際將將該元素作為 key 放入 HashMap。思考一下為什么?
由於 HashMap 的 put() 方法添加 key-value 對時,當新放入 HashMap 的 Entry 中 key 與集合中原有 Entry 的 key 相同(hashCode()返回值相等,通過 equals 比較也返回 true),新添加的 Entry 的 value 會將覆蓋原來 Entry 的 value(HashSet 中的 value 都是PRESENT),但 key 不會有任何改變,因此如果向 HashSet 中添加一個已經存在的元素時,新添加的集合元素將不會被放入 HashMap中,原來的元素也不會有任何改變,這也就滿足了 Set 中元素不重復的特性。
該方法如果添加的是在 HashSet 中不存在的,則返回 true;如果添加的元素已經存在,返回 false。其原因在於我們之前提到的關於 HashMap 的 put 方法。該方法在添加 key 不重復的鍵值對的時候,會返回 null。
其余方法
-
/**
-
* 如果此set包含指定元素,則返回true。
-
* 更確切地講,當且僅當此set包含一個滿足(o==null ? e==null : o.equals(e))的e元素時,返回true。
-
*
-
* 底層實際調用HashMap的containsKey判斷是否包含指定key。
-
* @param o 在此set中的存在已得到測試的元素。
-
* @return 如果此set包含指定元素,則返回true。
-
*/
-
public boolean contains(Object o) {
-
return map.containsKey(o);
-
}
-
/**
-
* 如果指定元素存在於此set中,則將其移除。更確切地講,如果此set包含一個滿足(o==null ? e==null : o.equals(e))的元素e,
-
* 則將其移除。如果此set已包含該元素,則返回true
-
*
-
* 底層實際調用HashMap的remove方法刪除指定Entry。
-
* @param o 如果存在於此set中則需要將其移除的對象。
-
* @return 如果set包含指定元素,則返回true。
-
*/
-
public boolean remove(Object o) {
-
return map.remove(o)==PRESENT;
-
}
-
/**
-
* 返回此HashSet實例的淺表副本:並沒有復制這些元素本身。
-
*
-
* 底層實際調用HashMap的clone()方法,獲取HashMap的淺表副本,並設置到HashSet中。
-
*/
-
public Object clone() {
-
try {
-
HashSet<E> newSet = (HashSet<E>) super.clone();
-
newSet.map = (HashMap<E, Object>) map.clone();
-
return newSet;
-
} catch (CloneNotSupportedException e) {
-
throw new InternalError();
-
}
-
}
-
}
相關說明
- 相關 HashMap 的實現原理,請參考我的上一遍總結:HashMap的實現原理。
- 對於 HashSet 中保存的對象,請注意正確重寫其 equals 和 hashCode 方法,以保證放入的對象的唯一性。這兩個方法是比較重要的,希望大家在以后的開發過程中需要注意一下。
Hashtable 的實現原理
概述
和 HashMap 一樣,Hashtable 也是一個散列表,它存儲的內容是鍵值對。
Hashtable 在 Java 中的定義為:
-
public class Hashtable<K,V>
-
extends Dictionary<K,V>
-
implements Map<K,V>, Cloneable, java.io.Serializable{}
從源碼中,我們可以看出,Hashtable 繼承於 Dictionary 類,實現了 Map, Cloneable, java.io.Serializable接口。其中Dictionary類是任何可將鍵映射到相應值的類(如 Hashtable)的抽象父類,每個鍵和值都是對象(源碼注釋為:The Dictionary class is the abstract parent of any class, such as Hashtable, which maps keys to values. Every key and every value is an object.)。但在這一點我開始有點懷疑,因為我查看了HashMap以及TreeMap的源碼,都沒有繼承於這個類。不過當我看到注釋中的解釋也就明白了,其 Dictionary 源碼注釋是這樣的:NOTE: This class is obsolete. New implementations should implement the Map interface, rather than extending this class. 該話指出 Dictionary 這個類過時了,新的實現類應該實現Map接口。
Hashtable 源碼解讀
成員變量
Hashtable是通過"拉鏈法"實現的哈希表。它包括幾個重要的成員變量:table, count, threshold, loadFactor, modCount。
- table是一個 Entry[] 數組類型,而 Entry(在 HashMap 中有講解過)實際上就是一個單向鏈表。哈希表的"key-value鍵值對"都是存儲在Entry數組中的。
- count 是 Hashtable 的大小,它是 Hashtable 保存的鍵值對的數量。
- threshold 是 Hashtable 的閾值,用於判斷是否需要調整 Hashtable 的容量。threshold 的值="容量*加載因子"。
- loadFactor 就是加載因子。
- modCount 是用來實現 fail-fast 機制的。
關於變量的解釋在源碼注釋中都有,最好還是應該看英文注釋。
-
/**
-
* The hash table data.
-
*/
-
private transient Entry<K,V>[] table;
-
-
/**
-
* The total number of entries in the hash table.
-
*/
-
private transient int count;
-
-
/**
-
* The table is rehashed when its size exceeds this threshold. (The
-
* value of this field is (int)(capacity * loadFactor).)
-
*
-
* @serial
-
*/
-
private int threshold;
-
-
/**
-
* The load factor for the hashtable.
-
*
-
* @serial
-
*/
-
private float loadFactor;
-
-
/**
-
* The number of times this Hashtable has been structurally modified
-
* Structural modifications are those that change the number of entries in
-
* the Hashtable or otherwise modify its internal structure (e.g.,
-
* rehash). This field is used to make iterators on Collection-views of
-
* the Hashtable fail-fast. (See ConcurrentModificationException).
-
*/
-
private transient int modCount = 0;
構造方法
Hashtable 一共提供了 4 個構造方法:
public Hashtable(int initialCapacity, float loadFactor): 用指定初始容量和指定加載因子構造一個新的空哈希表。useAltHashing 為 boolean,其如果為真,則執行另一散列的字符串鍵,以減少由於弱哈希計算導致的哈希沖突的發生。public Hashtable(int initialCapacity):用指定初始容量和默認的加載因子 (0.75) 構造一個新的空哈希表。public Hashtable():默認構造函數,容量為 11,加載因子為 0.75。public Hashtable(Map<? extends K, ? extends V> t):構造一個與給定的 Map 具有相同映射關系的新哈希表。
-
/**
-
* Constructs a new, empty hashtable with the specified initial
-
* capacity and the specified load factor.
-
*
-
* @param initialCapacity the initial capacity of the hashtable.
-
* @param loadFactor the load factor of the hashtable.
-
* @exception IllegalArgumentException if the initial capacity is less
-
* than zero, or if the load factor is nonpositive.
-
*/
-
public Hashtable(int initialCapacity, float loadFactor) {
-
if (initialCapacity < 0)
-
throw new IllegalArgumentException("Illegal Capacity: "+
-
initialCapacity);
-
if (loadFactor <= 0 || Float.isNaN(loadFactor))
-
throw new IllegalArgumentException("Illegal Load: "+loadFactor);
-
-
if (initialCapacity==0)
-
initialCapacity = 1;
-
this.loadFactor = loadFactor;
-
table = new Entry[initialCapacity];
-
threshold = ( int)Math.min(initialCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
-
useAltHashing = sun.misc.VM.isBooted() &&
-
(initialCapacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
-
}
-
-
/**
-
* Constructs a new, empty hashtable with the specified initial capacity
-
* and default load factor (0.75).
-
*
-
* @param initialCapacity the initial capacity of the hashtable.
-
* @exception IllegalArgumentException if the initial capacity is less
-
* than zero.
-
*/
-
public Hashtable(int initialCapacity) {
-
this(initialCapacity, 0.75f);
-
}
-
-
/**
-
* Constructs a new, empty hashtable with a default initial capacity (11)
-
* and load factor (0.75).
-
*/
-
public Hashtable() {
-
this(11, 0.75f);
-
}
-
-
/**
-
* Constructs a new hashtable with the same mappings as the given
-
* Map. The hashtable is created with an initial capacity sufficient to
-
* hold the mappings in the given Map and a default load factor (0.75).
-
*
-
* @param t the map whose mappings are to be placed in this map.
-
* @throws NullPointerException if the specified map is null.
-
* @since 1.2
-
*/
-
public Hashtable(Map<? extends K, ? extends V> t) {
-
this(Math.max(2*t.size(), 11), 0.75f);
-
putAll(t);
-
}
put 方法
put 方法的整個流程為:
- 判斷 value 是否為空,為空則拋出異常;
- 計算 key 的 hash 值,並根據 hash 值獲得 key 在 table 數組中的位置 index,如果 table[index] 元素不為空,則進行迭代,如果遇到相同的 key,則直接替換,並返回舊 value;
- 否則,我們可以將其插入到 table[index] 位置。
我在下面的代碼中也進行了一些注釋:
-
public synchronized V put(K key, V value) {
-
// Make sure the value is not null確保value不為null
-
if (value == null) {
-
throw new NullPointerException();
-
}
-
-
// Makes sure the key is not already in the hashtable.
-
//確保key不在hashtable中
-
//首先,通過hash方法計算key的哈希值,並計算得出index值,確定其在table[]中的位置
-
//其次,迭代index索引位置的鏈表,如果該位置處的鏈表存在相同的key,則替換value,返回舊的value
-
Entry tab[] = table;
-
int hash = hash(key);
-
int index = (hash & 0x7FFFFFFF) % tab.length;
-
for (Entry<K,V> e = tab[index] ; e != null ; e = e.next) {
-
if ((e.hash == hash) && e.key.equals(key)) {
-
V old = e.value;
-
e.value = value;
-
return old;
-
}
-
}
-
-
modCount++;
-
if (count >= threshold) {
-
// Rehash the table if the threshold is exceeded
-
//如果超過閥值,就進行rehash操作
-
rehash();
-
-
tab = table;
-
hash = hash(key);
-
index = (hash & 0x7FFFFFFF) % tab.length;
-
}
-
-
// Creates the new entry.
-
//將值插入,返回的為null
-
Entry<K,V> e = tab[index];
-
// 創建新的Entry節點,並將新的Entry插入Hashtable的index位置,並設置e為新的Entry的下一個元素
-
tab[index] = new Entry<>(hash, key, value, e);
-
count++;
-
return null;
-
}
通過一個實際的例子來演示一下這個過程:
假設我們現在Hashtable的容量為5,已經存在了(5,5),(13,13),(16,16),(17,17),(21,21)這 5 個鍵值對,目前他們在Hashtable中的位置如下:
現在,我們插入一個新的鍵值對,put(16,22),假設key=16的索引為1.但現在索引1的位置有兩個Entry了,所以程序會對鏈表進行迭代。迭代的過程中,發現其中有一個Entry的key和我們要插入的鍵值對的key相同,所以現在會做的工作就是將newValue=22替換oldValue=16,然后返回oldValue=16.
然后我們現在再插入一個,put(33,33),key=33的索引為3,並且在鏈表中也不存在key=33的Entry,所以將該節點插入鏈表的第一個位置。
get 方法
相比較於 put 方法,get 方法則簡單很多。其過程就是首先通過 hash()方法求得 key 的哈希值,然后根據 hash 值得到 index 索引(上述兩步所用的算法與 put 方法都相同)。然后迭代鏈表,返回匹配的 key 的對應的 value;找不到則返回 null。
-
public synchronized V get(Object key) {
-
Entry tab[] = table;
-
int hash = hash(key);
-
int index = (hash & 0x7FFFFFFF) % tab.length;
-
for (Entry<K,V> e = tab[index] ; e != null ; e = e.next) {
-
if ((e.hash == hash) && e.key.equals(key)) {
-
return e.value;
-
}
-
}
-
return null;
-
}
Hashtable 遍歷方式
Hashtable 有多種遍歷方式:
-
//1、使用keys()
-
Enumeration<String> en1 = table.keys();
-
while(en1.hasMoreElements()) {
-
en1.nextElement();
-
}
-
-
//2、使用elements()
-
Enumeration<String> en2 = table.elements();
-
while(en2.hasMoreElements()) {
-
en2.nextElement();
-
}
-
-
//3、使用keySet()
-
Iterator<String> it1 = table.keySet().iterator();
-
while(it1.hasNext()) {
-
it1.next();
-
}
-
-
//4、使用entrySet()
-
Iterator<Entry<String, String>> it2 = table.entrySet().iterator();
-
while(it2.hasNext()) {
-
it2.next();
-
}
Hashtable 與 HashMap 的簡單比較
- HashTable 基於 Dictionary 類,而 HashMap 是基於 AbstractMap。Dictionary 是任何可將鍵映射到相應值的類的抽象父類,而 AbstractMap 是基於 Map 接口的實現,它以最大限度地減少實現此接口所需的工作。
- HashMap 的 key 和 value 都允許為 null,而 Hashtable 的 key 和 value 都不允許為 null。HashMap 遇到 key 為 null 的時候,調用 putForNullKey 方法進行處理,而對 value 沒有處理;Hashtable遇到 null,直接返回 NullPointerException。
- Hashtable 方法是同步,而HashMap則不是。我們可以看一下源碼,Hashtable 中的幾乎所有的 public 的方法都是 synchronized 的,而有些方法也是在內部通過 synchronized 代碼塊來實現。所以有人一般都建議如果是涉及到多線程同步時采用 HashTable,沒有涉及就采用 HashMap,但是在 Collections 類中存在一個靜態方法:synchronizedMap(),該方法創建了一個線程安全的 Map 對象,並把它作為一個封裝的對象來返回。
LinkedHashMap 的實現原理
LinkedHashMap 概述
HashMap 是無序的,HashMap 在 put 的時候是根據 key 的 hashcode 進行 hash 然后放入對應的地方。所以在按照一定順序 put 進 HashMap 中,然后遍歷出 HashMap 的順序跟 put 的順序不同(除非在 put 的時候 key 已經按照 hashcode 排序號了,這種幾率非常小)
JAVA 在 JDK1.4 以后提供了 LinkedHashMap 來幫助我們實現了有序的 HashMap!
LinkedHashMap 是 HashMap 的一個子類,它保留插入的順序,如果需要輸出的順序和輸入時的相同,那么就選用 LinkedHashMap。
LinkedHashMap 是 Map 接口的哈希表和鏈接列表實現,具有可預知的迭代順序。此實現提供所有可選的映射操作,並允許使用 null 值和 null 鍵。此類不保證映射的順序,特別是它不保證該順序恆久不變。
LinkedHashMap 實現與 HashMap 的不同之處在於,LinkedHashMap 維護着一個運行於所有條目的雙重鏈接列表。此鏈接列表定義了迭代順序,該迭代順序可以是插入順序或者是訪問順序。
注意,此實現不是同步的。如果多個線程同時訪問鏈接的哈希映射,而其中至少一個線程從結構上修改了該映射,則它必須保持外部同步。
根據鏈表中元素的順序可以分為:按插入順序的鏈表,和按訪問順序(調用 get 方法)的鏈表。默認是按插入順序排序,如果指定按訪問順序排序,那么調用get方法后,會將這次訪問的元素移至鏈表尾部,不斷訪問可以形成按訪問順序排序的鏈表。
小 Demo
我在最開始學習 LinkedHashMap 的時候,看到訪問順序、插入順序等等,有點暈了,隨着后續的學習才慢慢懂得其中原理,所以我會先在進行做幾個 demo 來演示一下 LinkedHashMap 的使用。看懂了其效果,然后再來研究其原理。
HashMap
看下面這個代碼:
-
public static void main(String[] args) {
-
Map<String, String> map = new HashMap<String, String>();
-
map.put( "apple", "蘋果");
-
map.put( "watermelon", "西瓜");
-
map.put( "banana", "香蕉");
-
map.put( "peach", "桃子");
-
-
Iterator iter = map.entrySet().iterator();
-
while (iter.hasNext()) {
-
Map.Entry entry = (Map.Entry) iter.next();
-
System.out.println(entry.getKey() + "=" + entry.getValue());
-
}
-
}
一個比較簡單的測試 HashMap 的代碼,通過控制台的輸出,我們可以看到 HashMap 是沒有順序的。
-
banana=香蕉
-
apple=蘋果
-
peach=桃子
-
watermelon=西瓜
LinkedHashMap
我們現在將 map 的實現換成 LinkedHashMap,其他代碼不變:Map<String, String> map = new LinkedHashMap<String, String>();
看一下控制台的輸出:
-
apple=蘋果
-
watermelon=西瓜
-
banana=香蕉
-
peach=桃子
我們可以看到,其輸出順序是完成按照插入順序的!也就是我們上面所說的保留了插入的順序。我們不是在上面還提到過其可以按照訪問順序進行排序么?好的,我們還是通過一個例子來驗證一下:
-
public static void main(String[] args) {
-
Map<String, String> map = new LinkedHashMap<String, String>(16,0.75f,true);
-
map.put( "apple", "蘋果");
-
map.put( "watermelon", "西瓜");
-
map.put( "banana", "香蕉");
-
map.put( "peach", "桃子");
-
-
map.get( "banana");
-
map.get( "apple");
-
-
Iterator iter = map.entrySet().iterator();
-
while (iter.hasNext()) {
-
Map.Entry entry = (Map.Entry) iter.next();
-
System.out.println(entry.getKey() + "=" + entry.getValue());
-
}
-
}
代碼與之前的都差不多,但我們多了兩行代碼,並且初始化 LinkedHashMap 的時候,用的構造函數也不相同,看一下控制台的輸出結果:
-
watermelon=西瓜
-
peach=桃子
-
banana=香蕉
-
apple=蘋果
這也就是我們之前提到過的,LinkedHashMap 可以選擇按照訪問順序進行排序。
LinkedHashMap 的實現
對於 LinkedHashMap 而言,它繼承與 HashMap(public class LinkedHashMap<K,V> extends HashMap<K,V> implements Map<K,V>)、底層使用哈希表與雙向鏈表來保存所有元素。其基本操作與父類 HashMap 相似,它通過重寫父類相關的方法,來實現自己的鏈接列表特性。下面我們來分析 LinkedHashMap 的源代碼:
成員變量
LinkedHashMap 采用的 hash 算法和 HashMap 相同,但是它重新定義了數組中保存的元素 Entry,該 Entry 除了保存當前對象的引用外,還保存了其上一個元素 before 和下一個元素 after 的引用,從而在哈希表的基礎上又構成了雙向鏈接列表。看源代碼:
-
/**
-
* The iteration ordering method for this linked hash map: <tt>true</tt>
-
* for access-order, <tt>false</tt> for insertion-order.
-
* 如果為true,則按照訪問順序;如果為false,則按照插入順序。
-
*/
-
private final boolean accessOrder;
-
/**
-
* 雙向鏈表的表頭元素。
-
*/
-
private transient Entry<K,V> header;
-
-
/**
-
* LinkedHashMap的Entry元素。
-
* 繼承HashMap的Entry元素,又保存了其上一個元素before和下一個元素after的引用。
-
*/
-
private static class Entry<K,V> extends HashMap.Entry<K,V> {
-
Entry<K,V> before, after;
-
……
-
}
LinkedHashMap 中的 Entry 集成與 HashMap 的 Entry,但是其增加了 before 和 after 的引用,指的是上一個元素和下一個元素的引用。
初始化
通過源代碼可以看出,在 LinkedHashMap 的構造方法中,實際調用了父類 HashMap 的相關構造方法來構造一個底層存放的 table 數組,但額外可以增加 accessOrder 這個參數,如果不設置,默認為 false,代表按照插入順序進行迭代;當然可以顯式設置為 true,代表以訪問順序進行迭代。如:
-
public LinkedHashMap(int initialCapacity, float loadFactor,boolean accessOrder) {
-
super(initialCapacity, loadFactor);
-
this.accessOrder = accessOrder;
-
}
我們已經知道 LinkedHashMap 的 Entry 元素繼承 HashMap 的 Entry,提供了雙向鏈表的功能。在上述 HashMap 的構造器中,最后會調用 init() 方法,進行相關的初始化,這個方法在 HashMap 的實現中並無意義,只是提供給子類實現相關的初始化調用。
但在 LinkedHashMap 重寫了 init() 方法,在調用父類的構造方法完成構造后,進一步實現了對其元素 Entry 的初始化操作。
-
/**
-
* Called by superclass constructors and pseudoconstructors (clone,
-
* readObject) before any entries are inserted into the map. Initializes
-
* the chain.
-
*/
-
-
void init() {
-
header = new Entry<>(-1, null, null, null);
-
header.before = header.after = header;
-
}
存儲
LinkedHashMap 並未重寫父類 HashMap 的 put 方法,而是重寫了父類 HashMap 的 put 方法調用的子方法void recordAccess(HashMap m) ,void addEntry(int hash, K key, V value, int bucketIndex) 和void createEntry(int hash, K key, V value, int bucketIndex),提供了自己特有的雙向鏈接列表的實現。我們在之前的文章中已經講解了HashMap的put方法,我們在這里重新貼一下 HashMap 的 put 方法的源代碼:
HashMap.put:
-
public V put(K key, V value) {
-
if (key == null)
-
return putForNullKey(value);
-
int hash = hash(key);
-
int i = indexFor(hash, table.length);
-
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
-
Object k;
-
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
-
V oldValue = e.value;
-
e.value = value;
-
e.recordAccess( this);
-
return oldValue;
-
}
-
}
-
-
modCount++;
-
addEntry(hash, key, value, i);
-
return null;
-
}
重寫方法:
-
void recordAccess(HashMap<K,V> m) {
-
LinkedHashMap<K,V> lm = (LinkedHashMap<K,V>)m;
-
if (lm.accessOrder) {
-
lm.modCount++;
-
remove();
-
addBefore(lm.header);
-
}
-
}
-
-
void addEntry(int hash, K key, V value, int bucketIndex) {
-
// 調用create方法,將新元素以雙向鏈表的的形式加入到映射中。
-
createEntry(hash, key, value, bucketIndex);
-
-
// 刪除最近最少使用元素的策略定義
-
Entry<K,V> eldest = header.after;
-
if (removeEldestEntry(eldest)) {
-
removeEntryForKey(eldest.key);
-
} else {
-
if (size >= threshold)
-
resize( 2 * table.length);
-
}
-
}
-
-
void createEntry(int hash, K key, V value, int bucketIndex) {
-
HashMap.Entry<K,V> old = table[bucketIndex];
-
Entry<K,V> e = new Entry<K,V>(hash, key, value, old);
-
table[bucketIndex] = e;
-
// 調用元素的addBrefore方法,將元素加入到哈希、雙向鏈接列表。
-
e.addBefore(header);
-
size++;
-
}
-
-
private void addBefore(Entry<K,V> existingEntry) {
-
after = existingEntry;
-
before = existingEntry.before;
-
before.after = this;
-
after.before = this;
-
}
讀取
LinkedHashMap 重寫了父類 HashMap 的 get 方法,實際在調用父類 getEntry() 方法取得查找的元素后,再判斷當排序模式 accessOrder 為 true 時,記錄訪問順序,將最新訪問的元素添加到雙向鏈表的表頭,並從原來的位置刪除。由於的鏈表的增加、刪除操作是常量級的,故並不會帶來性能的損失。
-
public V get(Object key) {
-
// 調用父類HashMap的getEntry()方法,取得要查找的元素。
-
Entry<K,V> e = (Entry<K,V>)getEntry(key);
-
if (e == null)
-
return null;
-
// 記錄訪問順序。
-
e.recordAccess( this);
-
return e.value;
-
}
-
-
void recordAccess(HashMap<K,V> m) {
-
LinkedHashMap<K,V> lm = (LinkedHashMap<K,V>)m;
-
// 如果定義了LinkedHashMap的迭代順序為訪問順序,
-
// 則刪除以前位置上的元素,並將最新訪問的元素添加到鏈表表頭。
-
if (lm.accessOrder) {
-
lm.modCount++;
-
remove();
-
addBefore(lm.header);
-
}
-
}
-
-
/**
-
* Removes this entry from the linked list.
-
*/
-
private void remove() {
-
before.after = after;
-
after.before = before;
-
}
-
-
/**clear鏈表,設置header為初始狀態*/
-
public void clear() {
-
super.clear();
-
header.before = header.after = header;
-
}
排序模式
LinkedHashMap 定義了排序模式 accessOrder,該屬性為 boolean 型變量,對於訪問順序,為 true;對於插入順序,則為 false。一般情況下,不必指定排序模式,其迭代順序即為默認為插入順序。
這些構造方法都會默認指定排序模式為插入順序。如果你想構造一個 LinkedHashMap,並打算按從近期訪問最少到近期訪問最多的順序(即訪問順序)來保存元素,那么請使用下面的構造方法構造 LinkedHashMap:public LinkedHashMap(int initialCapacity, float loadFactor, boolean accessOrder)
該哈希映射的迭代順序就是最后訪問其條目的順序,這種映射很適合構建 LRU 緩存。LinkedHashMap 提供了 removeEldestEntry(Map.Entry<K,V> eldest) 方法。該方法可以提供在每次添加新條目時移除最舊條目的實現程序,默認返回 false,這樣,此映射的行為將類似於正常映射,即永遠不能移除最舊的元素。
我們會在后面的文章中詳細介紹關於如何用 LinkedHashMap 構建 LRU 緩存。
總結
其實 LinkedHashMap 幾乎和 HashMap 一樣:從技術上來說,不同的是它定義了一個 Entry<K,V> header,這個 header 不是放在 Table 里,它是額外獨立出來的。LinkedHashMap 通過繼承 hashMap 中的 Entry<K,V>,並添加兩個屬性 Entry<K,V> before,after,和 header 結合起來組成一個雙向鏈表,來實現按插入順序或訪問順序排序。
在寫關於 LinkedHashMap 的過程中,記起來之前面試的過程中遇到的一個問題,也是問我 Map 的哪種實現可以做到按照插入順序進行迭代?當時腦子是突然短路的,但現在想想,也只能怪自己對這個知識點還是掌握的不夠扎實,所以又從頭認真的把代碼看了一遍。
不過,我的建議是,大家首先首先需要記住的是:LinkedHashMap 能夠做到按照插入順序或者訪問順序進行迭代,這樣在我們以后的開發中遇到相似的問題,才能想到用 LinkedHashMap 來解決,否則就算對其內部結構非常了解,不去使用也是沒有什么用的。
LinkedHashSet 的實現原理
LinkedHashSet 概述
思考了好久,到底要不要總結 LinkedHashSet 的內容 = = 我在之前的博文中,分別寫了 HashMap 和 HashSet,然后我們可以看到 HashSet 的方法基本上都是基於 HashMap 來實現的,說白了,HashSet內部的數據結構就是一個 HashMap,其方法的內部幾乎就是在調用 HashMap 的方法。
LinkedHashSet 首先我們需要知道的是它是一個 Set 的實現,所以它其中存的肯定不是鍵值對,而是值。此實現與 HashSet 的不同之處在於,LinkedHashSet 維護着一個運行於所有條目的雙重鏈接列表。此鏈接列表定義了迭代順序,該迭代順序可為插入順序或是訪問順序。
看到上面的介紹,是不是感覺其與 HashMap 和 LinkedHashMap 的關系很像?
注意,此實現不是同步的。如果多個線程同時訪問鏈接的哈希Set,而其中至少一個線程修改了該 Set,則它必須保持外部同步。
小 Demo
在LinkedHashMap的實現原理中,通過例子演示了 HashMap 和 LinkedHashMap 的區別。舉一反三,我們現在學習的LinkedHashSet與之前的很相同,只不過之前存的是鍵值對,而現在存的只有值。
所以我就不再具體的貼代碼在這邊了,但我們可以肯定的是,LinkedHashSet 是可以按照插入順序或者訪問順序進行迭代。
LinkedHashSet 的實現
對於 LinkedHashSet 而言,它繼承與 HashSet、又基於 LinkedHashMap 來實現的。
LinkedHashSet 底層使用 LinkedHashMap 來保存所有元素,它繼承與 HashSet,其所有的方法操作上又與 HashSet 相同,因此 LinkedHashSet 的實現上非常簡單,只提供了四個構造方法,並通過傳遞一個標識參數,調用父類的構造器,底層構造一個 LinkedHashMap 來實現,在相關操作上與父類 HashSet 的操作相同,直接調用父類 HashSet 的方法即可。LinkedHashSet 的源代碼如下:
-
public class LinkedHashSet<E>
-
extends HashSet<E>
-
implements Set<E>, Cloneable, java.io.Serializable {
-
-
private static final long serialVersionUID = -2851667679971038690L;
-
-
/**
-
* 構造一個帶有指定初始容量和加載因子的新空鏈接哈希set。
-
*
-
* 底層會調用父類的構造方法,構造一個有指定初始容量和加載因子的LinkedHashMap實例。
-
* @param initialCapacity 初始容量。
-
* @param loadFactor 加載因子。
-
*/
-
public LinkedHashSet(int initialCapacity, float loadFactor) {
-
super(initialCapacity, loadFactor, true);
-
}
-
-
/**
-
* 構造一個帶指定初始容量和默認加載因子0.75的新空鏈接哈希set。
-
*
-
* 底層會調用父類的構造方法,構造一個帶指定初始容量和默認加載因子0.75的LinkedHashMap實例。
-
* @param initialCapacity 初始容量。
-
*/
-
public LinkedHashSet(int initialCapacity) {
-
super(initialCapacity, .75f, true);
-
}
-
-
/**
-
* 構造一個帶默認初始容量16和加載因子0.75的新空鏈接哈希set。
-
*
-
* 底層會調用父類的構造方法,構造一個帶默認初始容量16和加載因子0.75的LinkedHashMap實例。
-
*/
-
public LinkedHashSet() {
-
super(16, .75f, true);
-
}
-
-
/**
-
* 構造一個與指定collection中的元素相同的新鏈接哈希set。
-
*
-
* 底層會調用父類的構造方法,構造一個足以包含指定collection
-
* 中所有元素的初始容量和加載因子為0.75的LinkedHashMap實例。
-
* @param c 其中的元素將存放在此set中的collection。
-
*/
-
public LinkedHashSet(Collection<? extends E> c) {
-
super(Math.max(2*c.size(), 11), .75f, true);
-
addAll(c);
-
}
-
}
以上幾乎就是 LinkedHashSet 的全部代碼了,那么讀者可能就會懷疑了,不是說 LinkedHashSet 是基於 LinkedHashMap 實現的嗎?那我為什么在源碼中甚至都沒有看到出現過 LinkedHashMap。不要着急,我們可以看到在 LinkedHashSet 的構造方法中,其調用了父類的構造方法。我們可以進去看一下:
-
/**
-
* 以指定的initialCapacity和loadFactor構造一個新的空鏈接哈希集合。
-
* 此構造函數為包訪問權限,不對外公開,實際只是是對LinkedHashSet的支持。
-
*
-
* 實際底層會以指定的參數構造一個空LinkedHashMap實例來實現。
-
* @param initialCapacity 初始容量。
-
* @param loadFactor 加載因子。
-
* @param dummy 標記。
-
*/
-
HashSet( int initialCapacity, float loadFactor, boolean dummy) {
-
map = new LinkedHashMap<E,Object>(initialCapacity, loadFactor);
-
}
在父類 HashSet 中,專為 LinkedHashSet 提供的構造方法如下,該方法為包訪問權限,並未對外公開。
由上述源代碼可見,LinkedHashSet 通過繼承 HashSet,底層使用 LinkedHashMap,以很簡單明了的方式來實現了其自身的所有功能。
總結
以上就是關於 LinkedHashSet 的內容,我們只是從概述上以及構造方法這幾個方面介紹了,並不是我們不想去深入其讀取或者寫入方法,而是其本身沒有實現,只是繼承於父類 HashSet 的方法。
所以我們需要注意的點是:
- LinkedHashSet 是 Set 的一個具體實現,其維護着一個運行於所有條目的雙重鏈接列表。此鏈接列表定義了迭代順序,該迭代順序可為插入順序或是訪問順序。
- LinkedHashSet 繼承與 HashSet,並且其內部是通過 LinkedHashMap 來實現的。有點類似於我們之前說的LinkedHashMap 其內部是基於 Hashmap 實現一樣,不過還是有一點點區別的(具體的區別大家可以自己去思考一下)。
- 如果我們需要迭代的順序為插入順序或者訪問順序,那么 LinkedHashSet 是需要你首先考慮的。
ArrayList 的實現原理
ArrayList 概述
ArrayList 可以理解為動態數組,用 MSDN 中的說法,就是 Array 的復雜版本。與 Java 中的數組相比,它的容量能動態增長。ArrayList 是 List 接口的可變數組的實現。實現了所有可選列表操作,並允許包括 null 在內的所有元素。除了實現 List 接口外,此類還提供一些方法來操作內部用來存儲列表的數組的大小。(此類大致上等同於 Vector 類,除了此類是不同步的。)
每個 ArrayList 實例都有一個容量,該容量是指用來存儲列表元素的數組的大小。它總是至少等於列表的大小。隨着向 ArrayList 中不斷添加元素,其容量也自動增長。自動增長會帶來數據向新數組的重新拷貝,因此,如果可預知數據量的多少,可在構造 ArrayList 時指定其容量。在添加大量元素前,應用程序也可以使用 ensureCapacity 操作來增加 ArrayList 實例的容量,這可以減少遞增式再分配的數量。
注意,此實現不是同步的。如果多個線程同時訪問一個 ArrayList 實例,而其中至少一個線程從結構上修改了列表,那么它必須保持外部同步。(結構上的修改是指任何添加或刪除一個或多個元素的操作,或者顯式調整底層數組的大小;僅僅設置元素的值不是結構上的修改。)
我們先學習了解其內部的實現原理,才能更好的理解其應用。
ArrayList 的實現
對於 ArrayList 而言,它實現 List 接口、底層使用數組保存所有元素。其操作基本上是對數組的操作。下面我們來分析 ArrayList 的源代碼:
實現的接口
-
public class ArrayList<E> extends AbstractList<E>
-
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
-
{
-
}
ArrayList 繼承了 AbstractList,實現了 List。它是一個數組隊列,提供了相關的添加、刪除、修改、遍歷等功能。
ArrayList 實現了 RandmoAccess 接口,即提供了隨機訪問功能。RandmoAccess 是 java 中用來被 List 實現,為 List 提供快速訪問功能的。在 ArrayList 中,我們即可以通過元素的序號快速獲取元素對象;這就是快速隨機訪問。
ArrayList 實現了 Cloneable 接口,即覆蓋了函數 clone(),能被克隆。 ArrayList 實現 java.io.Serializable 接口,這意味着 ArrayList 支持序列化,能通過序列化去傳輸。
底層使用數組實現
-
/**
-
* The array buffer into which the elements of the ArrayList are stored.
-
* The capacity of the ArrayList is the length of this array buffer.
-
*/
-
private transient Object[] elementData;
構造方法
-
-
/**
-
* Constructs an empty list with an initial capacity of ten.
-
*/
-
public ArrayList() {
-
this(10);
-
}
-
/**
-
* Constructs an empty list with the specified initial capacity.
-
*
-
* @param initialCapacity the initial capacity of the list
-
* @throws IllegalArgumentException if the specified initial capacity
-
* is negative
-
*/
-
public ArrayList(int initialCapacity) {
-
super();
-
if (initialCapacity < 0)
-
throw new IllegalArgumentException("Illegal Capacity: "+
-
initialCapacity);
-
this.elementData = new Object[initialCapacity];
-
}
-
-
/**
-
* Constructs a list containing the elements of the specified
-
* collection, in the order they are returned by the collection's
-
* iterator.
-
*
-
* @param c the collection whose elements are to be placed into this list
-
* @throws NullPointerException if the specified collection is null
-
*/
-
public ArrayList(Collection<? extends E> c) {
-
elementData = c.toArray();
-
size = elementData.length;
-
// c.toArray might (incorrectly) not return Object[] (see 6260652)
-
if (elementData.getClass() != Object[].class)
-
elementData = Arrays.copyOf(elementData, size, Object[].class);
-
}
ArrayList 提供了三種方式的構造器:
public ArrayList()可以構造一個默認初始容量為10的空列表;public ArrayList(int initialCapacity)構造一個指定初始容量的空列表;public ArrayList(Collection<? extends E> c)構造一個包含指定 collection 的元素的列表,這些元素按照該collection的迭代器返回它們的順序排列的。
存儲
ArrayList 中提供了多種添加元素的方法,下面將一一進行講解:
1.set(int index, E element):該方法首先調用rangeCheck(index)來校驗 index 變量是否超出數組范圍,超出則拋出異常。而后,取出原 index 位置的值,並且將新的 element 放入 Index 位置,返回 oldValue。
-
/**
-
* Replaces the element at the specified position in this list with
-
* the specified element.
-
*
-
* @param index index of the element to replace
-
* @param element element to be stored at the specified position
-
* @return the element previously at the specified position
-
* @throws IndexOutOfBoundsException {@inheritDoc}
-
*/
-
public E set(int index, E element) {
-
rangeCheck(index);
-
-
E oldValue = elementData(index);
-
elementData[index] = element;
-
return oldValue;
-
}
-
/**
-
* Checks if the given index is in range. If not, throws an appropriate
-
* runtime exception. This method does *not* check if the index is
-
* negative: It is always used immediately prior to an array access,
-
* which throws an ArrayIndexOutOfBoundsException if index is negative.
-
*/
-
private void rangeCheck(int index) {
-
if (index >= size)
-
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
-
}
2.add(E e):該方法是將指定的元素添加到列表的尾部。當容量不足時,會調用 grow 方法增長容量。
-
/**
-
* Appends the specified element to the end of this list.
-
*
-
* @param e element to be appended to this list
-
* @return <tt>true</tt> (as specified by {@link Collection#add})
-
*/
-
public boolean add(E e) {
-
ensureCapacityInternal(size + 1); // Increments modCount!!
-
elementData[size++] = e;
-
return true;
-
}
-
private void ensureCapacityInternal(int minCapacity) {
-
modCount++;
-
// overflow-conscious code
-
if (minCapacity - elementData.length > 0)
-
grow(minCapacity);
-
}
-
private void grow(int minCapacity) {
-
// overflow-conscious code
-
int oldCapacity = elementData.length;
-
int newCapacity = oldCapacity + (oldCapacity >> 1);
-
if (newCapacity - minCapacity < 0)
-
newCapacity = minCapacity;
-
if (newCapacity - MAX_ARRAY_SIZE > 0)
-
newCapacity = hugeCapacity(minCapacity);
-
// minCapacity is usually close to size, so this is a win:
-
elementData = Arrays.copyOf(elementData, newCapacity);
-
}
3.add(int index, E element):在 index 位置插入 element。
-
/**
-
* Inserts the specified element at the specified position in this
-
* list. Shifts the element currently at that position (if any) and
-
* any subsequent elements to the right (adds one to their indices).
-
*
-
* @param index index at which the specified element is to be inserted
-
* @param element element to be inserted
-
* @throws IndexOutOfBoundsException {@inheritDoc}
-
*/
-
public void add(int index, E element) {
-
rangeCheckForAdd(index);
-
-
ensureCapacityInternal(size + 1); // Increments modCount!!
-
System.arraycopy(elementData, index, elementData, index + 1,
-
size - index);
-
elementData[index] = element;
-
size++;
-
}
4.addAll(Collection<? extends E> c) 和 addAll(int index, Collection<? extends E> c):將特定 Collection 中的元素添加到 Arraylist 末尾。
-
/**
-
* Appends all of the elements in the specified collection to the end of
-
* this list, in the order that they are returned by the
-
* specified collection's Iterator. The behavior of this operation is
-
* undefined if the specified collection is modified while the operation
-
* is in progress. (This implies that the behavior of this call is
-
* undefined if the specified collection is this list, and this
-
* list is nonempty.)
-
*
-
* @param c collection containing elements to be added to this list
-
* @return <tt>true</tt> if this list changed as a result of the call
-
* @throws NullPointerException if the specified collection is null
-
*/
-
public boolean addAll(Collection<? extends E> c) {
-
Object[] a = c.toArray();
-
int numNew = a.length;
-
ensureCapacityInternal(size + numNew); // Increments modCount
-
System.arraycopy(a, 0, elementData, size, numNew);
-
size += numNew;
-
return numNew != 0;
-
}
-
-
/**
-
* Inserts all of the elements in the specified collection into this
-
* list, starting at the specified position. Shifts the element
-
* currently at that position (if any) and any subsequent elements to
-
* the right (increases their indices). The new elements will appear
-
* in the list in the order that they are returned by the
-
* specified collection's iterator.
-
*
-
* @param index index at which to insert the first element from the
-
* specified collection
-
* @param c collection containing elements to be added to this list
-
* @return <tt>true</tt> if this list changed as a result of the call
-
* @throws IndexOutOfBoundsException {@inheritDoc}
-
* @throws NullPointerException if the specified collection is null
-
*/
-
public boolean addAll(int index, Collection<? extends E> c) {
-
rangeCheckForAdd(index);
-
-
Object[] a = c.toArray();
-
int numNew = a.length;
-
ensureCapacityInternal(size + numNew); // Increments modCount
-
-
int numMoved = size - index;
-
if (numMoved > 0)
-
System.arraycopy(elementData, index, elementData, index + numNew,
-
numMoved);
-
-
System.arraycopy(a, 0, elementData, index, numNew);
-
size += numNew;
-
return numNew != 0;
-
}
在 ArrayList 的存儲方法,其核心本質是在數組的某個位置將元素添加進入。但其中又會涉及到關於數組容量不夠而增長等因素。
讀取
這個方法就比較簡單了,ArrayList 能夠支持隨機訪問的原因也是很顯然的,因為它內部的數據結構是數組,而數組本身就是支持隨機訪問。該方法首先會判斷輸入的index值是否越界,然后將數組的 index 位置的元素返回即可。
-
/**
-
* Returns the element at the specified position in this list.
-
*
-
* @param index index of the element to return
-
* @return the element at the specified position in this list
-
* @throws IndexOutOfBoundsException {@inheritDoc}
-
*/
-
public E get(int index) {
-
rangeCheck(index);
-
return (E) elementData[index];
-
}
-
private void rangeCheck(int index) {
-
if (index >= size)
-
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
-
}
刪除
ArrayList 提供了根據下標或者指定對象兩種方式的刪除功能。需要注意的是該方法的返回值並不相同,如下:
-
/**
-
* Removes the element at the specified position in this list.
-
* Shifts any subsequent elements to the left (subtracts one from their
-
* indices).
-
*
-
* @param index the index of the element to be removed
-
* @return the element that was removed from the list
-
* @throws IndexOutOfBoundsException {@inheritDoc}
-
*/
-
public E remove(int index) {
-
rangeCheck(index);
-
-
modCount++;
-
E oldValue = elementData(index);
-
-
int numMoved = size - index - 1;
-
if (numMoved > 0)
-
System.arraycopy(elementData, index+ 1, elementData, index,
-
numMoved);
-
elementData[--size] = null; // Let gc do its work
-
-
return oldValue;
-
}
-
/**
-
* Removes the first occurrence of the specified element from this list,
-
* if it is present. If the list does not contain the element, it is
-
* unchanged. More formally, removes the element with the lowest index
-
* <tt>i</tt> such that
-
* <tt>(o==null ? get(i)==null : o.equals(get(i)))</tt>
-
* (if such an element exists). Returns <tt>true</tt> if this list
-
* contained the specified element (or equivalently, if this list
-
* changed as a result of the call).
-
*
-
* @param o element to be removed from this list, if present
-
* @return <tt>true</tt> if this list contained the specified element
-
*/
-
public boolean remove(Object o) {
-
if (o == null) {
-
for (int index = 0; index < size; index++)
-
if (elementData[index] == null) {
-
fastRemove(index);
-
return true;
-
}
-
} else {
-
for (int index = 0; index < size; index++)
-
if (o.equals(elementData[index])) {
-
fastRemove(index);
-
return true;
-
}
-
}
-
return false;
-
}
注意:從數組中移除元素的操作,也會導致被移除的元素以后的所有元素的向左移動一個位置。
調整數組容量
從上面介紹的向 ArrayList 中存儲元素的代碼中,我們看到,每當向數組中添加元素時,都要去檢查添加后元素的個數是否會超出當前數組的長度,如果超出,數組將會進行擴容,以滿足添加數據的需求。數組擴容有兩個方法,其中開發者可以通過一個 public 的方法ensureCapacity(int minCapacity)來增加 ArrayList 的容量,而在存儲元素等操作過程中,如果遇到容量不足,會調用priavte方法private void ensureCapacityInternal(int minCapacity)實現。
-
public void ensureCapacity(int minCapacity) {
-
if (minCapacity > 0)
-
ensureCapacityInternal(minCapacity);
-
}
-
-
private void ensureCapacityInternal(int minCapacity) {
-
modCount++;
-
// overflow-conscious code
-
if (minCapacity - elementData.length > 0)
-
grow(minCapacity);
-
}
-
/**
-
* Increases the capacity to ensure that it can hold at least the
-
* number of elements specified by the minimum capacity argument.
-
*
-
* @param minCapacity the desired minimum capacity
-
*/
-
private void grow(int minCapacity) {
-
// overflow-conscious code
-
int oldCapacity = elementData.length;
-
int newCapacity = oldCapacity + (oldCapacity >> 1);
-
if (newCapacity - minCapacity < 0)
-
newCapacity = minCapacity;
-
if (newCapacity - MAX_ARRAY_SIZE > 0)
-
newCapacity = hugeCapacity(minCapacity);
-
// minCapacity is usually close to size, so this is a win:
-
elementData = Arrays.copyOf(elementData, newCapacity);
-
}
從上述代碼中可以看出,數組進行擴容時,會將老數組中的元素重新拷貝一份到新的數組中,每次數組容量的增長大約是其原容量的 1.5 倍(從int newCapacity = oldCapacity + (oldCapacity >> 1)這行代碼得出)。這種操作的代價是很高的,因此在實際使用時,我們應該盡量避免數組容量的擴張。當我們可預知要保存的元素的多少時,要在構造 ArrayList 實例時,就指定其容量,以避免數組擴容的發生。或者根據實際需求,通過調用ensureCapacity 方法來手動增加 ArrayList 實例的容量。
Fail-Fast 機制
ArrayList 也采用了快速失敗的機制,通過記錄 modCount 參數來實現。在面對並發的修改時,迭代器很快就會完全失敗,而不是冒着在將來某個不確定時間發生任意不確定行為的風險。 關於 Fail-Fast 的更詳細的介紹,我在之前將 HashMap 中已經提到。
LinkedList 的實現原理
概述
LinkedList 和 ArrayList 一樣,都實現了 List 接口,但其內部的數據結構有本質的不同。LinkedList 是基於鏈表實現的(通過名字也能區分開來),所以它的插入和刪除操作比 ArrayList 更加高效。但也是由於其為基於鏈表的,所以隨機訪問的效率要比 ArrayList 差。
看一下 LinkedList 的類的定義:
-
public class LinkedList<E>
-
extends AbstractSequentialList<E>
-
implements List<E>, Deque<E>, Cloneable, java.io.Serializable
-
{}
LinkedList 繼承自 AbstractSequenceList,實現了 List、Deque、Cloneable、java.io.Serializable 接口。AbstractSequenceList 提供了List接口骨干性的實現以減少實現 List 接口的復雜度,Deque 接口定義了雙端隊列的操作。
在 LinkedList 中除了本身自己的方法外,還提供了一些可以使其作為棧、隊列或者雙端隊列的方法。這些方法可能彼此之間只是名字不同,以使得這些名字在特定的環境中顯得更加合適。
LinkedList 也是 fail-fast 的(前邊提過很多次了)。
LinkedList 源碼解讀
數據結構
LinkedList 是基於鏈表結構實現,所以在類中包含了 first 和 last 兩個指針(Node)。Node 中包含了上一個節點和下一個節點的引用,這樣就構成了雙向的鏈表。每個 Node 只能知道自己的前一個節點和后一個節點,但對於鏈表來說,這已經足夠了。
-
transient int size = 0;
-
transient Node<E> first; //鏈表的頭指針
-
transient Node<E> last; //尾指針
-
//存儲對象的結構 Node, 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;
-
}
-
}
存儲
add(E e)
該方法是在鏈表的 end 添加元素,其調用了自己的方法 linkLast(E e)。
該方法首先將 last 的 Node 引用指向了一個新的 Node(l),然后根據l新建了一個 newNode,其中的元素就為要添加的 e;而后,我們讓 last 指向了 newNode。接下來是自身進行維護該鏈表。
-
/**
-
* Appends the specified element to the end of this list.
-
*
-
* <p>This method is equivalent to {@link #addLast}.
-
*
-
* @param e element to be appended to this list
-
* @return {@code true} (as specified by {@link Collection#add})
-
*/
-
public boolean add(E e) {
-
linkLast(e);
-
return true;
-
}
-
/**
-
* Links e as last element.
-
*/
-
void linkLast(E e) {
-
final Node<E> l = last;
-
final Node<E> newNode = new Node<>(l, e, null);
-
last = newNode;
-
if (l == null)
-
first = newNode;
-
else
-
l.next = newNode;
-
size++;
-
modCount++;
-
}
add(int index, E element)
該方法是在指定 index 位置插入元素。如果 index 位置正好等於 size,則調用 linkLast(element) 將其插入末尾;否則調用 linkBefore(element, node(index))方法進行插入。該方法的實現在下面,大家可以自己仔細的分析一下。(分析鏈表的時候最好能夠邊畫圖邊分析)
-
/**
-
* Inserts the specified element at the specified position in this list.
-
* Shifts the element currently at that position (if any) and any
-
* subsequent elements to the right (adds one to their indices).
-
*
-
* @param index index at which the specified element is to be inserted
-
* @param element element to be inserted
-
* @throws IndexOutOfBoundsException {@inheritDoc}
-
*/
-
public void add(int index, E element) {
-
checkPositionIndex(index);
-
-
if (index == size)
-
linkLast(element);
-
else
-
linkBefore(element, node(index));
-
}
-
/**
-
* Inserts element e before non-null Node succ.
-
*/
-
void linkBefore(E e, Node<E> succ) {
-
// assert succ != null;
-
final Node<E> pred = succ.prev;
-
final Node<E> newNode = new Node<>(pred, e, succ);
-
succ.prev = newNode;
-
if (pred == null)
-
first = newNode;
-
else
-
pred.next = newNode;
-
size++;
-
modCount++;
-
}
LinkedList 的方法實在是太多,在這沒法一一舉例分析。但很多方法其實都只是在調用別的方法而已,所以建議大家將其幾個最核心的添加的方法搞懂就可以了,比如 linkBefore、linkLast。其本質也就是鏈表之間的刪除添加等。
ConcurrentHashMap 的實現原理
概述
我們在之前的博文中了解到關於 HashMap 和 Hashtable 這兩種集合。其中 HashMap 是非線程安全的,當我們只有一個線程在使用 HashMap 的時候,自然不會有問題,但如果涉及到多個線程,並且有讀有寫的過程中,HashMap 就不能滿足我們的需要了(fail-fast)。在不考慮性能問題的時候,我們的解決方案有 Hashtable 或者Collections.synchronizedMap(hashMap),這兩種方式基本都是對整個 hash 表結構做鎖定操作的,這樣在鎖表的期間,別的線程就需要等待了,無疑性能不高。
所以我們在本文中學習一個 util.concurrent 包的重要成員,ConcurrentHashMap。
ConcurrentHashMap 的實現是依賴於 Java 內存模型,所以我們在了解 ConcurrentHashMap 的前提是必須了解Java 內存模型。但 Java 內存模型並不是本文的重點,所以我假設讀者已經對 Java 內存模型有所了解。
ConcurrentHashMap 分析
ConcurrentHashMap 的結構是比較復雜的,都深究去本質,其實也就是數組和鏈表而已。我們由淺入深慢慢的分析其結構。
先簡單分析一下,ConcurrentHashMap 的成員變量中,包含了一個 Segment 的數組(final Segment<K,V>[] segments;),而 Segment 是 ConcurrentHashMap 的內部類,然后在 Segment 這個類中,包含了一個 HashEntry 的數組(transient volatile HashEntry<K,V>[] table;)。而 HashEntry 也是 ConcurrentHashMap 的內部類。HashEntry 中,包含了 key 和 value 以及 next 指針(類似於 HashMap 中 Entry),所以 HashEntry 可以構成一個鏈表。
所以通俗的講,ConcurrentHashMap 數據結構為一個 Segment 數組,Segment 的數據結構為 HashEntry 的數組,而 HashEntry 存的是我們的鍵值對,可以構成鏈表。
首先,我們看一下 HashEntry 類。
HashEntry
HashEntry 用來封裝散列映射表中的鍵值對。在 HashEntry 類中,key,hash 和 next 域都被聲明為 final 型,value 域被聲明為 volatile 型。其類的定義為:
-
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;
-
}
-
...
-
...
-
}
HashEntry 的學習可以類比着 HashMap 中的 Entry。我們的存儲鍵值對的過程中,散列的時候如果發生“碰撞”,將采用“分離鏈表法”來處理碰撞:把碰撞的 HashEntry 對象鏈接成一個鏈表。
如下圖,我們在一個空桶中插入 A、B、C 兩個 HashEntry 對象后的結構圖(其實應該為鍵值對,在這進行了簡化以方便更容易理解):
Segment
Segment 的類定義為static final class Segment<K,V> extends ReentrantLock implements Serializable。其繼承於 ReentrantLock 類,從而使得 Segment 對象可以充當鎖的角色。Segment 中包含HashEntry 的數組,其可以守護其包含的若干個桶(HashEntry的數組)。Segment 在某些意義上有點類似於 HashMap了,都是包含了一個數組,而數組中的元素可以是一個鏈表。
table:table 是由 HashEntry 對象組成的數組如果散列時發生碰撞,碰撞的 HashEntry 對象就以鏈表的形式鏈接成一個鏈表table數組的數組成員代表散列映射表的一個桶每個 table 守護整個 ConcurrentHashMap 包含桶總數的一部分如果並發級別為 16,table 則守護 ConcurrentHashMap 包含的桶總數的 1/16。
count 變量是計算器,表示每個 Segment 對象管理的 table 數組(若干個 HashEntry 的鏈表)包含的HashEntry 對象的個數。之所以在每個Segment對象中包含一個 count 計數器,而不在 ConcurrentHashMap 中使用全局的計數器,是為了避免出現“熱點域”而影響並發性。
-
/**
-
* Segments are specialized versions of hash tables. This
-
* subclasses from ReentrantLock opportunistically, just to
-
* simplify some locking and avoid separate construction.
-
*/
-
static final class Segment<K,V> extends ReentrantLock implements Serializable {
-
/**
-
* The per-segment table. Elements are accessed via
-
* entryAt/setEntryAt providing volatile semantics.
-
*/
-
transient volatile HashEntry<K,V>[] table;
-
-
/**
-
* The number of elements. Accessed only either within locks
-
* or among other volatile reads that maintain visibility.
-
*/
-
transient int count;
-
transient int modCount;
-
/**
-
* 裝載因子
-
*/
-
final float loadFactor;
-
}
我們通過下圖來展示一下插入 ABC 三個節點后,Segment 的示意圖:
其實從我個人角度來說,Segment結構是與HashMap很像的。
ConcurrentHashMap
ConcurrentHashMap 的結構中包含的 Segment 的數組,在默認的並發級別會創建包含 16 個 Segment 對象的數組。通過我們上面的知識,我們知道每個 Segment 又包含若干個散列表的桶,每個桶是由 HashEntry 鏈接起來的一個鏈表。如果 key 能夠均勻散列,每個 Segment 大約守護整個散列表桶總數的 1/16。
下面我們還有通過一個圖來演示一下 ConcurrentHashMap 的結構:
並發寫操作
在 ConcurrentHashMap 中,當執行 put 方法的時候,會需要加鎖來完成。我們通過代碼來解釋一下具體過程: 當我們 new 一個 ConcurrentHashMap 對象,並且執行put操作的時候,首先會執行 ConcurrentHashMap 類中的 put 方法,該方法源碼為:
-
/**
-
* Maps the specified key to the specified value in this table.
-
* Neither the key nor the value can be null.
-
*
-
* <p> The value can be retrieved by calling the <tt>get</tt> method
-
* with a key that is equal to the original key.
-
*
-
* @param key key with which the specified value is to be associated
-
* @param value value to be associated with the specified key
-
* @return the previous value associated with <tt>key</tt>, or
-
* <tt>null</tt> if there was no mapping for <tt>key</tt>
-
* @throws NullPointerException if the specified key or value is null
-
*/
-
-
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 // nonvolatile; recheck
-
(segments, (j << SSHIFT) + SBASE)) == null) // in ensureSegment
-
s = ensureSegment(j);
-
return s.put(key, hash, value, false);
-
}
我們通過注釋可以了解到,ConcurrentHashMap 不允許空值。該方法首先有一個 Segment 的引用 s,然后會通過 hash() 方法對 key 進行計算,得到哈希值;繼而通過調用 Segment 的 put(K key, int hash, V value, boolean onlyIfAbsent)方法進行存儲操作。該方法源碼為:
-
final V put(K key, int hash, V value, boolean onlyIfAbsent) {
-
//加鎖,這里是鎖定的Segment而不是整個ConcurrentHashMap
-
HashEntry<K,V> node = tryLock() ? null :scanAndLockForPut(key, hash, value);
-
V oldValue;
-
try {
-
HashEntry<K,V>[] tab = table;
-
//得到hash對應的table中的索引index
-
int index = (tab.length - 1) & hash;
-
//找到hash對應的是具體的哪個桶,也就是哪個HashEntry鏈表
-
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;
-
}
關於該方法的某些關鍵步驟,在源碼上加上了注釋。
需要注意的是:加鎖操作是針對的 hash 值對應的某個 Segment,而不是整個 ConcurrentHashMap。因為 put 操作只是在這個 Segment 中完成,所以並不需要對整個 ConcurrentHashMap 加鎖。所以,此時,其他的線程也可以對另外的 Segment 進行 put 操作,因為雖然該 Segment 被鎖住了,但其他的 Segment 並沒有加鎖。同時,讀線程並不會因為本線程的加鎖而阻塞。
正是因為其內部的結構以及機制,所以 ConcurrentHashMap 在並發訪問的性能上要比Hashtable和同步包裝之后的HashMap的性能提高很多。在理想狀態下,ConcurrentHashMap 可以支持 16 個線程執行並發寫操作(如果並發級別設置為 16),及任意數量線程的讀操作。
總結
在實際的應用中,散列表一般的應用場景是:除了少數插入操作和刪除操作外,絕大多數都是讀取操作,而且讀操作在大多數時候都是成功的。正是基於這個前提,ConcurrentHashMap 針對讀操作做了大量的優化。通過 HashEntry 對象的不變性和用 volatile 型變量協調線程間的內存可見性,使得 大多數時候,讀操作不需要加鎖就可以正確獲得值。這個特性使得 ConcurrentHashMap 的並發性能在分離鎖的基礎上又有了近一步的提高。
ConcurrentHashMap 是一個並發散列映射表的實現,它允許完全並發的讀取,並且支持給定數量的並發更新。相比於 HashTable 和用同步包裝器包裝的 HashMap(Collections.synchronizedMap(new HashMap())),ConcurrentHashMap 擁有更高的並發性。在 HashTable 和由同步包裝器包裝的 HashMap 中,使用一個全局的鎖來同步不同線程間的並發訪問。同一時間點,只能有一個線程持有鎖,也就是說在同一時間點,只能有一個線程能訪問容器。這雖然保證多線程間的安全並發訪問,但同時也導致對容器的訪問變成串行化的了。
ConcurrentHashMap 的高並發性主要來自於三個方面:
- 用分離鎖實現多個線程間的更深層次的共享訪問。
- 用 HashEntery 對象的不變性來降低執行讀操作的線程在遍歷鏈表期間對加鎖的需求。
- 通過對同一個 Volatile 變量的寫 / 讀訪問,協調不同線程間讀 / 寫操作的內存可見性。
使用分離鎖,減小了請求 同一個鎖的頻率。
通過 HashEntery 對象的不變性及對同一個 Volatile 變量的讀 / 寫來協調內存可見性,使得 讀操作大多數時候不需要加鎖就能成功獲取到需要的值。由於散列映射表在實際應用中大多數操作都是成功的 讀操作,所以 2 和 3 既可以減少請求同一個鎖的頻率,也可以有效減少持有鎖的時間。通過減小請求同一個鎖的頻率和盡量減少持有鎖的時間 ,使得 ConcurrentHashMap 的並發性相對於 HashTable 和用同步包裝器包裝的 HashMap有了質的提高。
LinkedHashMap 與 LRUcache
LRU 緩存介紹
我們平時總會有一個電話本記錄所有朋友的電話,但是,如果有朋友經常聯系,那些朋友的電話號碼不用翻電話本我們也能記住,但是,如果長時間沒有聯系了,要再次聯系那位朋友的時候,我們又不得不求助電話本,但是,通過電話本查找還是很費時間的。但是,我們大腦能夠記住的東西是一定的,我們只能記住自己最熟悉的,而長時間不熟悉的自然就忘記了。
其實,計算機也用到了同樣的一個概念,我們用緩存來存放以前讀取的數據,而不是直接丟掉,這樣,再次讀取的時候,可以直接在緩存里面取,而不用再重新查找一遍,這樣系統的反應能力會有很大提高。但是,當我們讀取的個數特別大的時候,我們不可能把所有已經讀取的數據都放在緩存里,畢竟內存大小是一定的,我們一般把最近常讀取的放在緩存里(相當於我們把最近聯系的朋友的姓名和電話放在大腦里一樣)。
LRU 緩存利用了這樣的一種思想。LRU 是 Least Recently Used 的縮寫,翻譯過來就是“最近最少使用”,也就是說,LRU 緩存把最近最少使用的數據移除,讓給最新讀取的數據。而往往最常讀取的,也是讀取次數最多的,所以,利用 LRU 緩存,我們能夠提高系統的 performance。
實現
要實現 LRU 緩存,我們首先要用到一個類 LinkedHashMap。
用這個類有兩大好處:一是它本身已經實現了按照訪問順序的存儲,也就是說,最近讀取的會放在最前面,最最不常讀取的會放在最后(當然,它也可以實現按照插入順序存儲)。第二,LinkedHashMap 本身有一個方法用於判斷是否需要移除最不常讀取的數,但是,原始方法默認不需要移除(這是,LinkedHashMap 相當於一個linkedlist),所以,我們需要 override 這樣一個方法,使得當緩存里存放的數據個數超過規定個數后,就把最不常用的移除掉。關於 LinkedHashMap 中已經有詳細的介紹。
代碼如下:(可直接復制,也可以通過LRUcache-Java下載)
-
import java.util.LinkedHashMap;
-
import java.util.Collection;
-
import java.util.Map;
-
import java.util.ArrayList;
-
-
/**
-
* An LRU cache, based on <code>LinkedHashMap</code>.
-
*
-
* <p>
-
* This cache has a fixed maximum number of elements (<code>cacheSize</code>).
-
* If the cache is full and another entry is added, the LRU (least recently
-
* used) entry is dropped.
-
*
-
* <p>
-
* This class is thread-safe. All methods of this class are synchronized.
-
*
-
* <p>
-
* Author: Christian d'Heureuse, Inventec Informatik AG, Zurich, Switzerland<br>
-
* Multi-licensed: EPL / LGPL / GPL / AL / BSD.
-
*/
-
public class LRUCache<K, V> {
-
private static final float hashTableLoadFactor = 0.75f;
-
private LinkedHashMap<K, V> map;
-
private int cacheSize;
-
-
/**
-
* Creates a new LRU cache. 在該方法中,new LinkedHashMap<K,V>(hashTableCapacity,
-
* hashTableLoadFactor, true)中,true代表使用訪問順序
-
*
-
* @param cacheSize
-
* the maximum number of entries that will be kept in this cache.
-
*/
-
public LRUCache(int cacheSize) {
-
this.cacheSize = cacheSize;
-
int hashTableCapacity = (int) Math
-
.ceil(cacheSize / hashTableLoadFactor) + 1;
-
map = new LinkedHashMap<K, V>(hashTableCapacity, hashTableLoadFactor,
-
true) {
-
// (an anonymous inner class)
-
private static final long serialVersionUID = 1;
-
-
-
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
-
return size() > LRUCache.this.cacheSize;
-
}
-
};
-
}
-
-
/**
-
* Retrieves an entry from the cache.<br>
-
* The retrieved entry becomes the MRU (most recently used) entry.
-
*
-
* @param key
-
* the key whose associated value is to be returned.
-
* @return the value associated to this key, or null if no value with this
-
* key exists in the cache.
-
*/
-
public synchronized V get(K key) {
-
return map.get(key);
-
}
-
-
/**
-
* Adds an entry to this cache. The new entry becomes the MRU (most recently
-
* used) entry. If an entry with the specified key already exists in the
-
* cache, it is replaced by the new entry. If the cache is full, the LRU
-
* (least recently used) entry is removed from the cache.
-
*
-
* @param key
-
* the key with which the specified value is to be associated.
-
* @param value
-
* a value to be associated with the specified key.
-
*/
-
public synchronized void put(K key, V value) {
-
map.put(key, value);
-
}
-
-
/**
-
* Clears the cache.
-
*/
-
public synchronized void clear() {
-
map.clear();
-
}
-
-
/**
-
* Returns the number of used entries in the cache.
-
*
-
* @return the number of entries currently in the cache.
-
*/
-
public synchronized int usedEntries() {
-
return map.size();
-
}
-
-
/**
-
* Returns a <code>Collection</code> that contains a copy of all cache
-
* entries.
-
*
-
* @return a <code>Collection</code> with a copy of the cache content.
-
*/
-
public synchronized Collection<Map.Entry<K, V>> getAll() {
-
return new ArrayList<Map.Entry<K, V>>(map.entrySet());
-
}
-
-
// Test routine for the LRUCache class.
-
public static void main(String[] args) {
-
LRUCache<String, String> c = new LRUCache<String, String>(3);
-
c.put( "1", "one"); // 1
-
c.put( "2", "two"); // 2 1
-
c.put( "3", "three"); // 3 2 1
-
c.put( "4", "four"); // 4 3 2
-
if (c.get("2") == null)
-
throw new Error(); // 2 4 3
-
c.put( "5", "five"); // 5 2 4
-
c.put( "4", "second four"); // 4 5 2
-
// Verify cache content.
-
if (c.usedEntries() != 3)
-
throw new Error();
-
if (!c.get("4").equals("second four"))
-
throw new Error();
-
if (!c.get("5").equals("five"))
-
throw new Error();
-
if (!c.get("2").equals("two"))
-
throw new Error();
-
// List cache content.
-
for (Map.Entry<String, String> e : c.getAll())
-
System.out.println(e.getKey() + " : " + e.getValue());
-
}
-
}
HashSet 和 HashMap 的比較
HashMap 和 HashSet 都是 collection 框架的一部分,它們讓我們能夠使用對象的集合。collection 框架有自己的接口和實現,主要分為 Set 接口,List 接口和 Queue 接口。它們有各自的特點,Set 的集合里不允許對象有重復的值,List 允許有重復,它對集合中的對象進行索引,Queue 的工作原理是 FCFS 算法(First Come, First Serve)。
首先讓我們來看看什么是 HashMap 和 HashSet,然后再來比較它們之間的分別。
什么是 HashSet
HashSet 實現了 Set 接口,它不允許集合中有重復的值,當我們提到 HashSet 時,第一件事情就是在將對象存儲在 HashSet 之前,要先確保對象重寫 equals()和 hashCode()方法,這樣才能比較對象的值是否相等,以確保set中沒有儲存相等的對象。如果我們沒有重寫這兩個方法,將會使用這個方法的默認實現。
public boolean add(Obje
HashMap 的實現原理
HashMap 概述
HashMap 是基於哈希表的 Map 接口的非同步實現。此實現提供所有可選的映射操作,並允許使用 null 值和 null 鍵。此類不保證映射的順序,特別是它不保證該順序恆久不變。
此實現假定哈希函數將元素適當地分布在各桶之間,可為基本操作(get 和 put)提供穩定的性能。迭代 collection 視圖所需的時間與 HashMap 實例的“容量”(桶的數量)及其大小(鍵-值映射關系數)成比例。所以,如果迭代性能很重要,則不要將初始容量設置得太高或將加載因子設置得太低。也許大家開始對這段話有一點不太懂,不過不用擔心,當你讀完這篇文章后,就能深切理解這其中的含義了。
需要注意的是:Hashmap 不是同步的,如果多個線程同時訪問一個 HashMap,而其中至少一個線程從結構上(指添加或者刪除一個或多個映射關系的任何操作)修改了,則必須保持外部同步,以防止對映射進行意外的非同步訪問。
HashMap 的數據結構
在 Java 編程語言中,最基本的結構就是兩種,一個是數組,另外一個是指針(引用),HashMap 就是通過這兩個數據結構進行實現。HashMap實際上是一個“鏈表散列”的數據結構,即數組和鏈表的結合體。
從上圖中可以看出,HashMap 底層就是一個數組結構,數組中的每一項又是一個鏈表。當新建一個 HashMap 的時候,就會初始化一個數組。
我們通過 JDK 中的 HashMap 源碼進行一些學習,首先看一下構造函數:
-
public HashMap(int initialCapacity, float loadFactor) {
-
if (initialCapacity < 0)
-
throw new IllegalArgumentException("Illegal initial capacity: " +
-
initialCapacity);
-
if (initialCapacity > MAXIMUM_CAPACITY)
-
initialCapacity = MAXIMUM_CAPACITY;
-
if (loadFactor <= 0 || Float.isNaN(loadFactor))
-
throw new IllegalArgumentException("Illegal load factor: " +
-
loadFactor);
-
-
// Find a power of 2 >= initialCapacity
-
int capacity = 1;
-
while (capacity < initialCapacity)
-
capacity <<= 1;
-
-
this.loadFactor = loadFactor;
-
threshold = ( int)Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
-
table = new Entry[capacity];
-
useAltHashing = sun.misc.VM.isBooted() &&
-
(capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
-
init();
-
}
我們着重看一下第 18 行代碼table = new Entry[capacity];。這不就是 Java 中數組的創建方式嗎?也就是說在構造函數中,其創建了一個 Entry 的數組,其大小為 capacity(目前我們還不需要太了解該變量含義),那么 Entry 又是什么結構呢?看一下源碼:
-
static class Entry<K,V> implements Map.Entry<K,V> {
-
final K key;
-
V value;
-
Entry<K,V> next;
-
final int hash;
-
……
-
}
我們目前還是只着重核心的部分,Entry 是一個 static class,其中包含了 key 和 value,也就是鍵值對,另外還包含了一個 next 的 Entry 指針。我們可以總結出:Entry 就是數組中的元素,每個 Entry 其實就是一個 key-value 對,它持有一個指向下一個元素的引用,這就構成了鏈表。
HashMap 的核心方法解讀
存儲
-
/**
-
* Associates the specified value with the specified key in this map.
-
* If the map previously contained a mapping for the key, the old
-
* value is replaced.
-
*
-
* @param key key with which the specified value is to be associated
-
* @param value value to be associated with the specified key
-
* @return the previous value associated with <tt>key</tt>, or
-
* <tt>null</tt> if there was no mapping for <tt>key</tt>.
-
* (A <tt>null</tt> return can also indicate that the map
-
* previously associated <tt>null</tt> with <tt>key</tt>.)
-
*/
-
public V put(K key, V value) {
-
//其允許存放null的key和null的value,當其key為null時,調用putForNullKey方法,放入到table[0]的這個位置
-
if (key == null)
-
return putForNullKey(value);
-
//通過調用hash方法對key進行哈希,得到哈希之后的數值。該方法實現可以通過看源碼,其目的是為了盡可能的讓鍵值對可以分不到不同的桶中
-
int hash = hash(key);
-
//根據上一步驟中求出的hash得到在數組中是索引i
-
int i = indexFor(hash, table.length);
-
//如果i處的Entry不為null,則通過其next指針不斷遍歷e元素的下一個元素。
-
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
-
Object k;
-
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
-
V oldValue = e.value;
-
e.value = value;
-
e.recordAccess( this);
-
return oldValue;
-
}
-
}
-
-
modCount++;
-
addEntry(hash, key, value, i);
-
return null;
-
}
我們看一下方法的標准注釋:在注釋中首先提到了,當我們 put 的時候,如果 key 存在了,那么新的 value 會代替舊的 value,並且如果 key 存在的情況下,該方法返回的是舊的 value,如果 key 不存在,那么返回 null。
從上面的源代碼中可以看出:當我們往 HashMap 中 put 元素的時候,先根據 key 的 hashCode 重新計算 hash 值,根據 hash 值得到這個元素在數組中的位置(即下標),如果數組該位置上已經存放有其他元素了,那么在這個位置上的元素將以鏈表的形式存放,新加入的放在鏈頭,最先加入的放在鏈尾。如果數組該位置上沒有元素,就直接將該元素放到此數組中的該位置上。
addEntry(hash, key, value, i)方法根據計算出的 hash 值,將 key-value 對放在數組 table 的 i 索引處。addEntry 是 HashMap 提供的一個包訪問權限的方法,代碼如下:
-
/**
-
* Adds a new entry with the specified key, value and hash code to
-
* the specified bucket. It is the responsibility of this
-
* method to resize the table if appropriate.
-
*
-
* Subclass overrides this to alter the behavior of put method.
-
*/
-
void addEntry(int hash, K key, V value, int bucketIndex) {
-
if ((size >= threshold) && (null != table[bucketIndex])) {
-
resize( 2 * table.length);
-
hash = ( null != key) ? hash(key) : 0;
-
bucketIndex = indexFor(hash, table.length);
-
}
-
-
createEntry(hash, key, value, bucketIndex);
-
}
-
void createEntry(int hash, K key, V value, int bucketIndex) {
-
// 獲取指定 bucketIndex 索引處的 Entry
-
Entry<K,V> e = table[bucketIndex];
-
// 將新創建的 Entry 放入 bucketIndex 索引處,並讓新的 Entry 指向原來的 Entr
-
table[bucketIndex] = new Entry<>(hash, key, value, e);
-
size++;
-
}
當系統決定存儲 HashMap 中的 key-value 對時,完全沒有考慮 Entry 中的 value,僅僅只是根據 key 來計算並決定每個 Entry 的存儲位置。我們完全可以把 Map 集合中的 value 當成 key 的附屬,當系統決定了 key 的存儲位置之后,value 隨之保存在那里即可。
hash(int h)方法根據 key 的 hashCode 重新計算一次散列。此算法加入了高位計算,防止低位不變,高位變化時,造成的 hash 沖突。
-
final int hash(Object k) {
-
int h = 0;
-
if (useAltHashing) {
-
if (k instanceof String) {
-
return sun.misc.Hashing.stringHash32((String) k);
-
}
-
h = hashSeed;
-
}
-
//得到k的hashcode值
-
h ^= k.hashCode();
-
//進行計算
-
h ^= (h >>> 20) ^ (h >>> 12);
-
return h ^ (h >>> 7) ^ (h >>> 4);
-
}
我們可以看到在 HashMap 中要找到某個元素,需要根據 key 的 hash 值來求得對應數組中的位置。如何計算這個位置就是 hash 算法。前面說過 HashMap 的數據結構是數組和鏈表的結合,所以我們當然希望這個 HashMap 里面的 元素位置盡量的分布均勻些,盡量使得每個位置上的元素數量只有一個,那么當我們用 hash 算法求得這個位置的時候,馬上就可以知道對應位置的元素就是我們要的,而不用再去遍歷鏈表,這樣就大大優化了查詢的效率。
對於任意給定的對象,只要它的 hashCode() 返回值相同,那么程序調用 hash(int h) 方法所計算得到的 hash 碼值總是相同的。我們首先想到的就是把 hash 值對數組長度取模運算,這樣一來,元素的分布相對來說是比較均勻的。但是,“模”運算的消耗還是比較大的,在 HashMap 中是這樣做的:調用 indexFor(int h, int length) 方法來計算該對象應該保存在 table 數組的哪個索引處。indexFor(int h, int length) 方法的代碼如下:
-
/**
-
* Returns index for hash code h.
-
*/
-
static int indexFor(int h, int length) {
-
return h & (length-1);
-
}
這個方法非常巧妙,它通過 h & (table.length -1) 來得到該對象的保存位,而 HashMap 底層數組的長度總是 2 的 n 次方,這是 HashMap 在速度上的優化。在 HashMap 構造器中有如下代碼:
-
// Find a power of 2 >= initialCapacity
-
int capacity = 1;
-
while (capacity < initialCapacity)
-
capacity <<= 1;
這段代碼保證初始化時 HashMap 的容量總是 2 的 n 次方,即底層數組的長度總是為 2 的 n 次方。
當 length 總是 2 的 n 次方時,h& (length-1)運算等價於對 length 取模,也就是 h%length,但是 & 比 % 具有更高的效率。這看上去很簡單,其實比較有玄機的,我們舉個例子來說明:
假設數組長度分別為 15 和 16,優化后的 hash 碼分別為 8 和 9,那么 & 運算后的結果如下:
| h & (table.length-1) | hash | table.length-1 | ||
|---|---|---|---|---|
| 8 & (15-1): | 0100 | & | 1110 | = 0100 |
| 9 & (15-1): | 0101 | & | 1110 | = 0100 |
| 8 & (16-1): | 0100 | & | 1111 | = 0100 |
| 9 & (16-1): | 0101 | & | 1111 | = 0101 |
從上面的例子中可以看出:當它們和 15-1(1110)“與”的時候,產生了相同的結果,也就是說它們會定位到數組中的同一個位置上去,這就產生了碰撞,8 和 9 會被放到數組中的同一個位置上形成鏈表,那么查詢的時候就需要遍歷這個鏈 表,得到8或者9,這樣就降低了查詢的效率。同時,我們也可以發現,當數組長度為 15 的時候,hash 值會與 15-1(1110)進行“與”,那么最后一位永遠是 0,而 0001,0011,0101,1001,1011,0111,1101 這幾個位置永遠都不能存放元素了,空間浪費相當大,更糟的是這種情況中,數組可以使用的位置比數組長度小了很多,這意味着進一步增加了碰撞的幾率,減慢了查詢的效率!而當數組長度為16時,即為2的n次方時,2n-1 得到的二進制數的每個位上的值都為 1,這使得在低位上&時,得到的和原 hash 的低位相同,加之 hash(int h)方法對 key 的 hashCode 的進一步優化,加入了高位計算,就使得只有相同的 hash 值的兩個值才會被放到數組中的同一個位置上形成鏈表。
所以說,當數組長度為 2 的 n 次冪的時候,不同的 key 算得得 index 相同的幾率較小,那么數據在數組上分布就比較均勻,也就是說碰撞的幾率小,相對的,查詢的時候就不用遍歷某個位置上的鏈表,這樣查詢效率也就較高了。
根據上面 put 方法的源代碼可以看出,當程序試圖將一個key-value對放入HashMap中時,程序首先根據該 key 的 hashCode() 返回值決定該 Entry 的存儲位置:如果兩個 Entry 的 key 的 hashCode() 返回值相同,那它們的存儲位置相同。如果這兩個 Entry 的 key 通過 equals 比較返回 true,新添加 Entry 的 value 將覆蓋集合中原有 Entry 的 value,但key不會覆蓋。如果這兩個 Entry 的 key 通過 equals 比較返回 false,新添加的 Entry 將與集合中原有 Entry 形成 Entry 鏈,而且新添加的 Entry 位於 Entry 鏈的頭部——具體說明繼續看 addEntry() 方法的說明。
讀取
-
/**
-
* Returns the value to which the specified key is mapped,
-
* or {@code null} if this map contains no mapping for the key.
-
*
-
* <p>More formally, if this map contains a mapping from a key
-
* {@code k} to a value {@code v} such that {@code (key==null ? k==null :
-
* key.equals(k))}, then this method returns {@code v}; otherwise
-
* it returns {@code null}. (There can be at most one such mapping.)
-
*
-
* <p>A return value of {@code null} does not <i>necessarily</i>
-
* indicate that the map contains no mapping for the key; it's also
-
* possible that the map explicitly maps the key to {@code null}.
-
* The {@link #containsKey containsKey} operation may be used to
-
* distinguish these two cases.
-
*
-
* @see #put(Object, Object)
-
*/
-
public V get(Object key) {
-
if (key == null)
-
return getForNullKey();
-
Entry<K,V> entry = getEntry(key);
-
-
return null == entry ? null : entry.getValue();
-
}
-
final Entry<K,V> getEntry(Object key) {
-
int hash = (key == null) ? 0 : hash(key);
-
for (Entry<K,V> e = table[indexFor(hash, table.length)];
-
e != null;
-
e = e.next) {
-
Object k;
-
if (e.hash == hash &&
-
((k = e.key) == key || (key != null && key.equals(k))))
-
return e;
-
}
-
return null;
-
}
有了上面存儲時的 hash 算法作為基礎,理解起來這段代碼就很容易了。從上面的源代碼中可以看出:從 HashMap 中 get 元素時,首先計算 key 的 hashCode,找到數組中對應位置的某一元素,然后通過 key 的 equals 方法在對應位置的鏈表中找到需要的元素。
歸納
簡單地說,HashMap 在底層將 key-value 當成一個整體進行處理,這個整體就是一個 Entry 對象。HashMap 底層采用一個 Entry[] 數組來保存所有的 key-value 對,當需要存儲一個 Entry 對象時,會根據 hash 算法來決定其在數組中的存儲位置,在根據 equals 方法決定其在該數組位置上的鏈表中的存儲位置;當需要取出一個Entry 時,也會根據 hash 算法找到其在數組中的存儲位置,再根據 equals 方法從該位置上的鏈表中取出該Entry。
HashMap 的 resize(rehash)
當 HashMap 中的元素越來越多的時候,hash 沖突的幾率也就越來越高,因為數組的長度是固定的。所以為了提高查詢的效率,就要對 HashMap 的數組進行擴容,數組擴容這個操作也會出現在 ArrayList 中,這是一個常用的操作,而在 HashMap 數組擴容之后,最消耗性能的點就出現了:原數組中的數據必須重新計算其在新數組中的位置,並放進去,這就是 resize。
那么 HashMap 什么時候進行擴容呢?當 HashMap 中的元素個數超過數組大小 *loadFactor時,就會進行數組擴容,loadFactor的默認值為 0.75,這是一個折中的取值。也就是說,默認情況下,數組大小為 16,那么當 HashMap 中元素個數超過 16*0.75=12 的時候,就把數組的大小擴展為 2*16=32,即擴大一倍,然后重新計算每個元素在數組中的位置,而這是一個非常消耗性能的操作,所以如果我們已經預知 HashMap 中元素的個數,那么預設元素的個數能夠有效的提高 HashMap 的性能。
HashMap 的性能參數
HashMap 包含如下幾個構造器:
- HashMap():構建一個初始容量為 16,負載因子為 0.75 的 HashMap。
- ashMap(int initialCapacity):構建一個初始容量為 initialCapacity,負載因子為 0.75 的 HashMap。
- HashMap(int initialCapacity, float loadFactor):以指定初始容量、指定的負載因子創建一個 HashMap。
HashMap 的基礎構造器 HashMap(int initialCapacity, float loadFactor) 帶有兩個參數,它們是初始容量 initialCapacity 和負載因子 loadFactor。
負載因子 loadFactor 衡量的是一個散列表的空間的使用程度,負載因子越大表示散列表的裝填程度越高,反之愈小。對於使用鏈表法的散列表來說,查找一個元素的平均時間是 O(1+a),因此如果負載因子越大,對空間的利用更充分,然而后果是查找效率的降低;如果負載因子太小,那么散列表的數據將過於稀疏,對空間造成嚴重浪費。
HashMap 的實現中,通過 threshold 字段來判斷 HashMap 的最大容量:
threshold = (int)(capacity * loadFactor);
結合負載因子的定義公式可知,threshold 就是在此 loadFactor 和 capacity 對應下允許的最大元素數目,超過這個數目就重新 resize,以降低實際的負載因子。默認的的負載因子 0.75 是對空間和時間效率的一個平衡選擇。當容量超出此最大容量時, resize 后的 HashMap 容量是容量的兩倍:
Fail-Fast 機制
原理
我們知道 java.util.HashMap 不是線程安全的,因此如果在使用迭代器的過程中有其他線程修改了 map,那么將拋出 ConcurrentModificationException,這就是所謂 fail-fast 策略。
ail-fast 機制是 java 集合(Collection)中的一種錯誤機制。 當多個線程對同一個集合的內容進行操作時,就可能會產生 fail-fast 事件。
例如:當某一個線程 A 通過 iterator去遍歷某集合的過程中,若該集合的內容被其他線程所改變了;那么線程 A 訪問集合時,就會拋出 ConcurrentModificationException 異常,產生 fail-fast 事件。
這一策略在源碼中的實現是通過 modCount 域,modCount 顧名思義就是修改次數,對 HashMap 內容(當然不僅僅是 HashMap 才會有,其他例如 ArrayList 也會)的修改都將增加這個值(大家可以再回頭看一下其源碼,在很多操作中都有 modCount++ 這句),那么在迭代器初始化過程中會將這個值賦給迭代器的 expectedModCount。
-
HashIterator() {
-
expectedModCount = modCount;
-
if (size > 0) { // advance to first entry
-
Entry[] t = table;
-
while (index < t.length && (next = t[index++]) == null)
-
;
-
}
-
}
在迭代過程中,判斷 modCount 跟 expectedModCount 是否相等,如果不相等就表示已經有其他線程修改了 Map:
注意到 modCount 聲明為 volatile,保證線程之間修改的可見性。
-
final Entry<K,V> nextEntry() {
-
if (modCount != expectedModCount)
-
throw new ConcurrentModificationException();
在 HashMap 的 API 中指出:
由所有 HashMap 類的“collection 視圖方法”所返回的迭代器都是快速失敗的:在迭代器創建之后,如果從結構上對映射進行修改,除非通過迭代器本身的 remove 方法,其他任何時間任何方式的修改,迭代器都將拋出 ConcurrentModificationException。因此,面對並發的修改,迭代器很快就會完全失敗,而不冒在將來不確定的時間發生任意不確定行為的風險。
注意,迭代器的快速失敗行為不能得到保證,一般來說,存在非同步的並發修改時,不可能作出任何堅決的保證。快速失敗迭代器盡最大努力拋出 ConcurrentModificationException。因此,編寫依賴於此異常的程序的做法是錯誤的,正確做法是:迭代器的快速失敗行為應該僅用於檢測程序錯誤。
解決方案
在上文中也提到,fail-fast 機制,是一種錯誤檢測機制。它只能被用來檢測錯誤,因為 JDK 並不保證 fail-fast 機制一定會發生。若在多線程環境下使用 fail-fast 機制的集合,建議使用“java.util.concurrent 包下的類”去取代“java.util 包下的類”。
HashMap 的兩種遍歷方式
第一種
-
Map map = new HashMap();
-
Iterator iter = map.entrySet().iterator();
-
while (iter.hasNext()) {
-
Map.Entry entry = (Map.Entry) iter.next();
-
Object key = entry.getKey();
-
Object val = entry.getValue();
-
}
效率高,以后一定要使用此種方式!
第二種
-
Map map = new HashMap();
-
Iterator iter = map.keySet().iterator();
-
while (iter.hasNext()) {
-
Object key = iter.next();
-
Object val = map.get(key);
-
}
效率低,以后盡量少使用!
HashSet 的實現原理
HashSet 概述
對於 HashSet 而言,它是基於 HashMap 實現的,底層采用 HashMap 來保存元素,所以如果對 HashMap 比較熟悉了,那么學習 HashSet 也是很輕松的。
我們先通過 HashSet 最簡單的構造函數和幾個成員變量來看一下,證明咱們上邊說的,其底層是 HashMap:
-
private transient HashMap<E,Object> map;
-
-
// Dummy value to associate with an Object in the backing Map
-
private static final Object PRESENT = new Object();
-
-
/**
-
* Constructs a new, empty set; the backing <tt>HashMap</tt> instance has
-
* default initial capacity (16) and load factor (0.75).
-
*/
-
public HashSet() {
-
map = new HashMap<>();
-
}
其實在英文注釋中已經說的比較明確了。首先有一個HashMap的成員變量,我們在 HashSet 的構造函數中將其初始化,默認情況下采用的是 initial capacity為16,load factor 為 0.75。
HashSet 的實現
對於 HashSet 而言,它是基於 HashMap 實現的,HashSet 底層使用 HashMap 來保存所有元素,因此 HashSet 的實現比較簡單,相關 HashSet 的操作,基本上都是直接調用底層 HashMap 的相關方法來完成,我們應該為保存到 HashSet 中的對象覆蓋 hashCode() 和 equals()
構造方法
-
/**
-
* 默認的無參構造器,構造一個空的HashSet。
-
*
-
* 實際底層會初始化一個空的HashMap,並使用默認初始容量為16和加載因子0.75。
-
*/
-
public HashSet() {
-
map = new HashMap<E,Object>();
-
}
-
-
/**
-
* 構造一個包含指定collection中的元素的新set。
-
*
-
* 實際底層使用默認的加載因子0.75和足以包含指定collection中所有元素的初始容量來創建一個HashMap。
-
* @param c 其中的元素將存放在此set中的collection。
-
*/
-
public HashSet(Collection<? extends E> c) {
-
map = new HashMap<E,Object>(Math.max((int) (c.size()/.75f) + 1, 16));
-
addAll(c);
-
}
-
-
/**
-
* 以指定的initialCapacity和loadFactor構造一個空的HashSet。
-
*
-
* 實際底層以相應的參數構造一個空的HashMap。
-
* @param initialCapacity 初始容量。
-
* @param loadFactor 加載因子。
-
*/
-
public HashSet(int initialCapacity, float loadFactor) {
-
map = new HashMap<E,Object>(initialCapacity, loadFactor);
-
}
-
-
/**
-
* 以指定的initialCapacity構造一個空的HashSet。
-
*
-
* 實際底層以相應的參數及加載因子loadFactor為0.75構造一個空的HashMap。
-
* @param initialCapacity 初始容量。
-
*/
-
public HashSet(int initialCapacity) {
-
map = new HashMap<E,Object>(initialCapacity);
-
}
-
-
/**
-
* 以指定的initialCapacity和loadFactor構造一個新的空鏈接哈希集合。此構造函數為包訪問權限,不對外公開,
-
* 實際只是是對LinkedHashSet的支持。
-
*
-
* 實際底層會以指定的參數構造一個空LinkedHashMap實例來實現。
-
* @param initialCapacity 初始容量。
-
* @param loadFactor 加載因子。
-
* @param dummy 標記。
-
*/
-
HashSet( int initialCapacity, float loadFactor, boolean dummy) {
-
map = new LinkedHashMap<E,Object>(initialCapacity, loadFactor);
-
}
add 方法
-
/**
-
-
* @param e 將添加到此set中的元素。
-
* @return 如果此set尚未包含指定元素,則返回true。
-
*/
-
public boolean add(E e) {
-
return map.put(e, PRESENT)==null;
-
}
如果此 set 中尚未包含指定元素,則添加指定元素。更確切地講,如果此 set 沒有包含滿足(e==null ? e2==null : e.equals(e2)) 的元素 e2,則向此 set 添加指定的元素 e。如果此 set 已包含該元素,則該調用不更改 set 並返回 false。但底層實際將將該元素作為 key 放入 HashMap。思考一下為什么?
由於 HashMap 的 put() 方法添加 key-value 對時,當新放入 HashMap 的 Entry 中 key 與集合中原有 Entry 的 key 相同(hashCode()返回值相等,通過 equals 比較也返回 true),新添加的 Entry 的 value 會將覆蓋原來 Entry 的 value(HashSet 中的 value 都是PRESENT),但 key 不會有任何改變,因此如果向 HashSet 中添加一個已經存在的元素時,新添加的集合元素將不會被放入 HashMap中,原來的元素也不會有任何改變,這也就滿足了 Set 中元素不重復的特性。
該方法如果添加的是在 HashSet 中不存在的,則返回 true;如果添加的元素已經存在,返回 false。其原因在於我們之前提到的關於 HashMap 的 put 方法。該方法在添加 key 不重復的鍵值對的時候,會返回 null。
其余方法
-
/**
-
* 如果此set包含指定元素,則返回true。
-
* 更確切地講,當且僅當此set包含一個滿足(o==null ? e==null : o.equals(e))的e元素時,返回true。
-
*
-
* 底層實際調用HashMap的containsKey判斷是否包含指定key。
-
* @param o 在此set中的存在已得到測試的元素。
-
* @return 如果此set包含指定元素,則返回true。
-
*/
-
public boolean contains(Object o) {
-
return map.containsKey(o);
-
}
-
/**
-
* 如果指定元素存在於此set中,則將其移除。更確切地講,如果此set包含一個滿足(o==null ? e==null : o.equals(e))的元素e,
-
* 則將其移除。如果此set已包含該元素,則返回true
-
*
-
* 底層實際調用HashMap的remove方法刪除指定Entry。
-
* @param o 如果存在於此set中則需要將其移除的對象。
-
* @return 如果set包含指定元素,則返回true。
-
*/
-
public boolean remove(Object o) {
-
return map.remove(o)==PRESENT;
-
}
-
/**
-
* 返回此HashSet實例的淺表副本:並沒有復制這些元素本身。
-
*
-
* 底層實際調用HashMap的clone()方法,獲取HashMap的淺表副本,並設置到HashSet中。
-
*/
-
public Object clone() {
-
try {
-
HashSet<E> newSet = (HashSet<E>) super.clone();
-
newSet.map = (HashMap<E, Object>) map.clone();
-
return newSet;
-
} catch (CloneNotSupportedException e) {
-
throw new InternalError();
-
}
-
}
-
}
相關說明
- 相關 HashMap 的實現原理,請參考我的上一遍總結:HashMap的實現原理。
- 對於 HashSet 中保存的對象,請注意正確重寫其 equals 和 hashCode 方法,以保證放入的對象的唯一性。這兩個方法是比較重要的,希望大家在以后的開發過程中需要注意一下。
Hashtable 的實現原理
概述
和 HashMap 一樣,Hashtable 也是一個散列表,它存儲的內容是鍵值對。
Hashtable 在 Java 中的定義為:
-
public class Hashtable<K,V>
-
extends Dictionary<K,V>
-
implements Map<K,V>, Cloneable, java.io.Serializable{}
從源碼中,我們可以看出,Hashtable 繼承於 Dictionary 類,實現了 Map, Cloneable, java.io.Serializable接口。其中Dictionary類是任何可將鍵映射到相應值的類(如 Hashtable)的抽象父類,每個鍵和值都是對象(源碼注釋為:The Dictionary class is the abstract parent of any class, such as Hashtable, which maps keys to values. Every key and every value is an object.)。但在這一點我開始有點懷疑,因為我查看了HashMap以及TreeMap的源碼,都沒有繼承於這個類。不過當我看到注釋中的解釋也就明白了,其 Dictionary 源碼注釋是這樣的:NOTE: This class is obsolete. New implementations should implement the Map interface, rather than extending this class. 該話指出 Dictionary 這個類過時了,新的實現類應該實現Map接口。
Hashtable 源碼解讀
成員變量
Hashtable是通過"拉鏈法"實現的哈希表。它包括幾個重要的成員變量:table, count, threshold, loadFactor, modCount。
- table是一個 Entry[] 數組類型,而 Entry(在 HashMap 中有講解過)實際上就是一個單向鏈表。哈希表的"key-value鍵值對"都是存儲在Entry數組中的。
- count 是 Hashtable 的大小,它是 Hashtable 保存的鍵值對的數量。
- threshold 是 Hashtable 的閾值,用於判斷是否需要調整 Hashtable 的容量。threshold 的值="容量*加載因子"。
- loadFactor 就是加載因子。
- modCount 是用來實現 fail-fast 機制的。
關於變量的解釋在源碼注釋中都有,最好還是應該看英文注釋。
-
/**
-
* The hash table data.
-
*/
-
private transient Entry<K,V>[] table;
-
-
/**
-
* The total number of entries in the hash table.
-
*/
-
private transient int count;
-
-
/**
-
* The table is rehashed when its size exceeds this threshold. (The
-
* value of this field is (int)(capacity * loadFactor).)
-
*
-
* @serial
-
*/
-
private int threshold;
-
-
/**
-
* The load factor for the hashtable.
-
*
-
* @serial
-
*/
-
private float loadFactor;
-
-
/**
-
* The number of times this Hashtable has been structurally modified
-
* Structural modifications are those that change the number of entries in
-
* the Hashtable or otherwise modify its internal structure (e.g.,
-
* rehash). This field is used to make iterators on Collection-views of
-
* the Hashtable fail-fast. (See ConcurrentModificationException).
-
*/
-
private transient int modCount = 0;
構造方法
Hashtable 一共提供了 4 個構造方法:
public Hashtable(int initialCapacity, float loadFactor): 用指定初始容量和指定加載因子構造一個新的空哈希表。useAltHashing 為 boolean,其如果為真,則執行另一散列的字符串鍵,以減少由於弱哈希計算導致的哈希沖突的發生。public Hashtable(int initialCapacity):用指定初始容量和默認的加載因子 (0.75) 構造一個新的空哈希表。public Hashtable():默認構造函數,容量為 11,加載因子為 0.75。public Hashtable(Map<? extends K, ? extends V> t):構造一個與給定的 Map 具有相同映射關系的新哈希表。
-
/**
-
* Constructs a new, empty hashtable with the specified initial
-
* capacity and the specified load factor.
-
*
-
* @param initialCapacity the initial capacity of the hashtable.
-
* @param loadFactor the load factor of the hashtable.
-
* @exception IllegalArgumentException if the initial capacity is less
-
* than zero, or if the load factor is nonpositive.
-
*/
-
public Hashtable(int initialCapacity, float loadFactor) {
-
if (initialCapacity < 0)
-
throw new IllegalArgumentException("Illegal Capacity: "+
-
initialCapacity);
-
if (loadFactor <= 0 || Float.isNaN(loadFactor))
-
throw new IllegalArgumentException("Illegal Load: "+loadFactor);
-
-
if (initialCapacity==0)
-
initialCapacity = 1;
-
this.loadFactor = loadFactor;
-
table = new Entry[initialCapacity];
-
threshold = ( int)Math.min(initialCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
-
useAltHashing = sun.misc.VM.isBooted() &&
-
(initialCapacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
-
}
-
-
/**
-
* Constructs a new, empty hashtable with the specified initial capacity
-
* and default load factor (0.75).
-
*
-
* @param initialCapacity the initial capacity of the hashtable.
-
* @exception IllegalArgumentException if the initial capacity is less
-
* than zero.
-
*/
-
public Hashtable(int initialCapacity) {
-
this(initialCapacity, 0.75f);
-
}
-
-
/**
-
* Constructs a new, empty hashtable with a default initial capacity (11)
-
* and load factor (0.75).
-
*/
-
public Hashtable() {
-
this(11, 0.75f);
-
}
-
-
/**
-
* Constructs a new hashtable with the same mappings as the given
-
* Map. The hashtable is created with an initial capacity sufficient to
-
* hold the mappings in the given Map and a default load factor (0.75).
-
*
-
* @param t the map whose mappings are to be placed in this map.
-
* @throws NullPointerException if the specified map is null.
-
* @since 1.2
-
*/
-
public Hashtable(Map<? extends K, ? extends V> t) {
-
this(Math.max(2*t.size(), 11), 0.75f);
-
putAll(t);
-
}
put 方法
put 方法的整個流程為:
- 判斷 value 是否為空,為空則拋出異常;
- 計算 key 的 hash 值,並根據 hash 值獲得 key 在 table 數組中的位置 index,如果 table[index] 元素不為空,則進行迭代,如果遇到相同的 key,則直接替換,並返回舊 value;
- 否則,我們可以將其插入到 table[index] 位置。
我在下面的代碼中也進行了一些注釋:
-
public synchronized V put(K key, V value) {
-
// Make sure the value is not null確保value不為null
-
if (value == null) {
-
throw new NullPointerException();
-
}
-
-
// Makes sure the key is not already in the hashtable.
-
//確保key不在hashtable中
-
//首先,通過hash方法計算key的哈希值,並計算得出index值,確定其在table[]中的位置
-
//其次,迭代index索引位置的鏈表,如果該位置處的鏈表存在相同的key,則替換value,返回舊的value
-
Entry tab[] = table;
-
int hash = hash(key);
-
int index = (hash & 0x7FFFFFFF) % tab.length;
-
for (Entry<K,V> e = tab[index] ; e != null ; e = e.next) {
-
if ((e.hash == hash) && e.key.equals(key)) {
-
V old = e.value;
-
e.value = value;
-
return old;
-
}
-
}
-
-
modCount++;
-
if (count >= threshold) {
-
// Rehash the table if the threshold is exceeded
-
//如果超過閥值,就進行rehash操作
-
rehash();
-
-
tab = table;
-
hash = hash(key);
-
index = (hash & 0x7FFFFFFF) % tab.length;
-
}
-
-
// Creates the new entry.
-
//將值插入,返回的為null
-
Entry<K,V> e = tab[index];
-
// 創建新的Entry節點,並將新的Entry插入Hashtable的index位置,並設置e為新的Entry的下一個元素
-
tab[index] = new Entry<>(hash, key, value, e);
-
count++;
-
return null;
-
}
通過一個實際的例子來演示一下這個過程:
假設我們現在Hashtable的容量為5,已經存在了(5,5),(13,13),(16,16),(17,17),(21,21)這 5 個鍵值對,目前他們在Hashtable中的位置如下:
現在,我們插入一個新的鍵值對,put(16,22),假設key=16的索引為1.但現在索引1的位置有兩個Entry了,所以程序會對鏈表進行迭代。迭代的過程中,發現其中有一個Entry的key和我們要插入的鍵值對的key相同,所以現在會做的工作就是將newValue=22替換oldValue=16,然后返回oldValue=16.
然后我們現在再插入一個,put(33,33),key=33的索引為3,並且在鏈表中也不存在key=33的Entry,所以將該節點插入鏈表的第一個位置。
get 方法
相比較於 put 方法,get 方法則簡單很多。其過程就是首先通過 hash()方法求得 key 的哈希值,然后根據 hash 值得到 index 索引(上述兩步所用的算法與 put 方法都相同)。然后迭代鏈表,返回匹配的 key 的對應的 value;找不到則返回 null。
-
public synchronized V get(Object key) {
-
Entry tab[] = table;
-
int hash = hash(key);
-
int index = (hash & 0x7FFFFFFF) % tab.length;
-
for (Entry<K,V> e = tab[index] ; e != null ; e = e.next) {
-
if ((e.hash == hash) && e.key.equals(key)) {
-
return e.value;
-
}
-
}
-
return null;
-
}
Hashtable 遍歷方式
Hashtable 有多種遍歷方式:
-
//1、使用keys()
-
Enumeration<String> en1 = table.keys();
-
while(en1.hasMoreElements()) {
-
en1.nextElement();
-
}
-
-
//2、使用elements()
-
Enumeration<String> en2 = table.elements();
-
while(en2.hasMoreElements()) {
-
en2.nextElement();
-
}
-
-
//3、使用keySet()
-
Iterator<String> it1 = table.keySet().iterator();
-
while(it1.hasNext()) {
-
it1.next();
-
}
-
-
//4、使用entrySet()
-
Iterator<Entry<String, String>> it2 = table.entrySet().iterator();
-
while(it2.hasNext()) {
-
it2.next();
-
}
Hashtable 與 HashMap 的簡單比較
- HashTable 基於 Dictionary 類,而 HashMap 是基於 AbstractMap。Dictionary 是任何可將鍵映射到相應值的類的抽象父類,而 AbstractMap 是基於 Map 接口的實現,它以最大限度地減少實現此接口所需的工作。
- HashMap 的 key 和 value 都允許為 null,而 Hashtable 的 key 和 value 都不允許為 null。HashMap 遇到 key 為 null 的時候,調用 putForNullKey 方法進行處理,而對 value 沒有處理;Hashtable遇到 null,直接返回 NullPointerException。
- Hashtable 方法是同步,而HashMap則不是。我們可以看一下源碼,Hashtable 中的幾乎所有的 public 的方法都是 synchronized 的,而有些方法也是在內部通過 synchronized 代碼塊來實現。所以有人一般都建議如果是涉及到多線程同步時采用 HashTable,沒有涉及就采用 HashMap,但是在 Collections 類中存在一個靜態方法:synchronizedMap(),該方法創建了一個線程安全的 Map 對象,並把它作為一個封裝的對象來返回。
LinkedHashMap 的實現原理
LinkedHashMap 概述
HashMap 是無序的,HashMap 在 put 的時候是根據 key 的 hashcode 進行 hash 然后放入對應的地方。所以在按照一定順序 put 進 HashMap 中,然后遍歷出 HashMap 的順序跟 put 的順序不同(除非在 put 的時候 key 已經按照 hashcode 排序號了,這種幾率非常小)
JAVA 在 JDK1.4 以后提供了 LinkedHashMap 來幫助我們實現了有序的 HashMap!
LinkedHashMap 是 HashMap 的一個子類,它保留插入的順序,如果需要輸出的順序和輸入時的相同,那么就選用 LinkedHashMap。
LinkedHashMap 是 Map 接口的哈希表和鏈接列表實現,具有可預知的迭代順序。此實現提供所有可選的映射操作,並允許使用 null 值和 null 鍵。此類不保證映射的順序,特別是它不保證該順序恆久不變。
LinkedHashMap 實現與 HashMap 的不同之處在於,LinkedHashMap 維護着一個運行於所有條目的雙重鏈接列表。此鏈接列表定義了迭代順序,該迭代順序可以是插入順序或者是訪問順序。
注意,此實現不是同步的。如果多個線程同時訪問鏈接的哈希映射,而其中至少一個線程從結構上修改了該映射,則它必須保持外部同步。
根據鏈表中元素的順序可以分為:按插入順序的鏈表,和按訪問順序(調用 get 方法)的鏈表。默認是按插入順序排序,如果指定按訪問順序排序,那么調用get方法后,會將這次訪問的元素移至鏈表尾部,不斷訪問可以形成按訪問順序排序的鏈表。
小 Demo
我在最開始學習 LinkedHashMap 的時候,看到訪問順序、插入順序等等,有點暈了,隨着后續的學習才慢慢懂得其中原理,所以我會先在進行做幾個 demo 來演示一下 LinkedHashMap 的使用。看懂了其效果,然后再來研究其原理。
HashMap
看下面這個代碼:
-
public static void main(String[] args) {
-
Map<String, String> map = new HashMap<String, String>();
-
map.put( "apple", "蘋果");
-
map.put( "watermelon", "西瓜");
-
map.put( "banana", "香蕉");
-
map.put( "peach", "桃子");
-
-
Iterator iter = map.entrySet().iterator();
-
while (iter.hasNext()) {
-
Map.Entry entry = (Map.Entry) iter.next();
-
System.out.println(entry.getKey() + "=" + entry.getValue());
-
}
-
}
一個比較簡單的測試 HashMap 的代碼,通過控制台的輸出,我們可以看到 HashMap 是沒有順序的。
-
banana=香蕉
-
apple=蘋果
-
peach=桃子
-
watermelon=西瓜
LinkedHashMap
我們現在將 map 的實現換成 LinkedHashMap,其他代碼不變:Map<String, String> map = new LinkedHashMap<String, String>();
看一下控制台的輸出:
-
apple=蘋果
-
watermelon=西瓜
-
banana=香蕉
-
peach=桃子
我們可以看到,其輸出順序是完成按照插入順序的!也就是我們上面所說的保留了插入的順序。我們不是在上面還提到過其可以按照訪問順序進行排序么?好的,我們還是通過一個例子來驗證一下:
-
public static void main(String[] args) {
-
Map<String, String> map = new LinkedHashMap<String, String>(16,0.75f,true);
-
map.put( "apple", "蘋果");
-
map.put( "watermelon", "西瓜");
-
map.put( "banana", "香蕉");
-
map.put( "peach", "桃子");
-
-
map.get( "banana");
-
map.get( "apple");
-
-
Iterator iter = map.entrySet().iterator();
-
while (iter.hasNext()) {
-
Map.Entry entry = (Map.Entry) iter.next();
-
System.out.println(entry.getKey() + "=" + entry.getValue());
-
}
-
}
代碼與之前的都差不多,但我們多了兩行代碼,並且初始化 LinkedHashMap 的時候,用的構造函數也不相同,看一下控制台的輸出結果:
-
watermelon=西瓜
-
peach=桃子
-
banana=香蕉
-
apple=蘋果
這也就是我們之前提到過的,LinkedHashMap 可以選擇按照訪問順序進行排序。
LinkedHashMap 的實現
對於 LinkedHashMap 而言,它繼承與 HashMap(public class LinkedHashMap<K,V> extends HashMap<K,V> implements Map<K,V>)、底層使用哈希表與雙向鏈表來保存所有元素。其基本操作與父類 HashMap 相似,它通過重寫父類相關的方法,來實現自己的鏈接列表特性。下面我們來分析 LinkedHashMap 的源代碼:
成員變量
LinkedHashMap 采用的 hash 算法和 HashMap 相同,但是它重新定義了數組中保存的元素 Entry,該 Entry 除了保存當前對象的引用外,還保存了其上一個元素 before 和下一個元素 after 的引用,從而在哈希表的基礎上又構成了雙向鏈接列表。看源代碼:
-
/**
-
* The iteration ordering method for this linked hash map: <tt>true</tt>
-
* for access-order, <tt>false</tt> for insertion-order.
-
* 如果為true,則按照訪問順序;如果為false,則按照插入順序。
-
*/
-
private final boolean accessOrder;
-
/**
-
* 雙向鏈表的表頭元素。
-
*/
-
private transient Entry<K,V> header;
-
-
/**
-
* LinkedHashMap的Entry元素。
-
* 繼承HashMap的Entry元素,又保存了其上一個元素before和下一個元素after的引用。
-
*/
-
private static class Entry<K,V> extends HashMap.Entry<K,V> {
-
Entry<K,V> before, after;
-
……
-
}
LinkedHashMap 中的 Entry 集成與 HashMap 的 Entry,但是其增加了 before 和 after 的引用,指的是上一個元素和下一個元素的引用。
初始化
通過源代碼可以看出,在 LinkedHashMap 的構造方法中,實際調用了父類 HashMap 的相關構造方法來構造一個底層存放的 table 數組,但額外可以增加 accessOrder 這個參數,如果不設置,默認為 false,代表按照插入順序進行迭代;當然可以顯式設置為 true,代表以訪問順序進行迭代。如:
-
public LinkedHashMap(int initialCapacity, float loadFactor,boolean accessOrder) {
-
super(initialCapacity, loadFactor);
-
this.accessOrder = accessOrder;
-
}
我們已經知道 LinkedHashMap 的 Entry 元素繼承 HashMap 的 Entry,提供了雙向鏈表的功能。在上述 HashMap 的構造器中,最后會調用 init() 方法,進行相關的初始化,這個方法在 HashMap 的實現中並無意義,只是提供給子類實現相關的初始化調用。
但在 LinkedHashMap 重寫了 init() 方法,在調用父類的構造方法完成構造后,進一步實現了對其元素 Entry 的初始化操作。
-
/**
-
* Called by superclass constructors and pseudoconstructors (clone,
-
* readObject) before any entries are inserted into the map. Initializes
-
* the chain.
-
*/
-
-
void init() {
-
header = new Entry<>(-1, null, null, null);
-
header.before = header.after = header;
-
}
存儲
LinkedHashMap 並未重寫父類 HashMap 的 put 方法,而是重寫了父類 HashMap 的 put 方法調用的子方法void recordAccess(HashMap m) ,void addEntry(int hash, K key, V value, int bucketIndex) 和void createEntry(int hash, K key, V value, int bucketIndex),提供了自己特有的雙向鏈接列表的實現。我們在之前的文章中已經講解了HashMap的put方法,我們在這里重新貼一下 HashMap 的 put 方法的源代碼:
HashMap.put:
-
public V put(K key, V value) {
-
if (key == null)
-
return putForNullKey(value);
-
int hash = hash(key);
-
int i = indexFor(hash, table.length);
-
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
-
Object k;
-
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
-
V oldValue = e.value;
-
e.value = value;
-
e.recordAccess( this);
-
return oldValue;
-
}
-
}
-
-
modCount++;
-
addEntry(hash, key, value, i);
-
return null;
-
}
重寫方法:
-
void recordAccess(HashMap<K,V> m) {
-
LinkedHashMap<K,V> lm = (LinkedHashMap<K,V>)m;
-
if (lm.accessOrder) {
-
lm.modCount++;
-
remove();
-
addBefore(lm.header);
-
}
-
}
-
-
void addEntry(int hash, K key, V value, int bucketIndex) {
-
// 調用create方法,將新元素以雙向鏈表的的形式加入到映射中。
-
createEntry(hash, key, value, bucketIndex);
-
-
// 刪除最近最少使用元素的策略定義
-
Entry<K,V> eldest = header.after;
-
if (removeEldestEntry(eldest)) {
-
removeEntryForKey(eldest.key);
-
} else {
-
if (size >= threshold)
-
resize( 2 * table.length);
-
}
-
}
-
-
void createEntry(int hash, K key, V value, int bucketIndex) {
-
HashMap.Entry<K,V> old = table[bucketIndex];
-
Entry<K,V> e = new Entry<K,V>(hash, key, value, old);
-
table[bucketIndex] = e;
-
// 調用元素的addBrefore方法,將元素加入到哈希、雙向鏈接列表。
-
e.addBefore(header);
-
size++;
-
}
-
-
private void addBefore(Entry<K,V> existingEntry) {
-
after = existingEntry;
-
before = existingEntry.before;
-
before.after = this;
-
after.before = this;
-
}
讀取
LinkedHashMap 重寫了父類 HashMap 的 get 方法,實際在調用父類 getEntry() 方法取得查找的元素后,再判斷當排序模式 accessOrder 為 true 時,記錄訪問順序,將最新訪問的元素添加到雙向鏈表的表頭,並從原來的位置刪除。由於的鏈表的增加、刪除操作是常量級的,故並不會帶來性能的損失。
-
public V get(Object key) {
-
// 調用父類HashMap的getEntry()方法,取得要查找的元素。
-
Entry<K,V> e = (Entry<K,V>)getEntry(key);
-
if (e == null)
-
return null;
-
// 記錄訪問順序。
-
e.recordAccess( this);
-
return e.value;
-
}
-
-
void recordAccess(HashMap<K,V> m) {
-
LinkedHashMap<K,V> lm = (LinkedHashMap<K,V>)m;
-
// 如果定義了LinkedHashMap的迭代順序為訪問順序,
-
// 則刪除以前位置上的元素,並將最新訪問的元素添加到鏈表表頭。
-
if (lm.accessOrder) {
-
lm.modCount++;
-
remove();
-
addBefore(lm.header);
-
}
-
}
-
-
/**
-
* Removes this entry from the linked list.
-
*/
-
private void remove() {
-
before.after = after;
-
after.before = before;
-
}
-
-
/**clear鏈表,設置header為初始狀態*/
-
public void clear() {
-
super.clear();
-
header.before = header.after = header;
-
}
排序模式
LinkedHashMap 定義了排序模式 accessOrder,該屬性為 boolean 型變量,對於訪問順序,為 true;對於插入順序,則為 false。一般情況下,不必指定排序模式,其迭代順序即為默認為插入順序。
這些構造方法都會默認指定排序模式為插入順序。如果你想構造一個 LinkedHashMap,並打算按從近期訪問最少到近期訪問最多的順序(即訪問順序)來保存元素,那么請使用下面的構造方法構造 LinkedHashMap:public LinkedHashMap(int initialCapacity, float loadFactor, boolean accessOrder)
該哈希映射的迭代順序就是最后訪問其條目的順序,這種映射很適合構建 LRU 緩存。LinkedHashMap 提供了 removeEldestEntry(Map.Entry<K,V> eldest) 方法。該方法可以提供在每次添加新條目時移除最舊條目的實現程序,默認返回 false,這樣,此映射的行為將類似於正常映射,即永遠不能移除最舊的元素。
我們會在后面的文章中詳細介紹關於如何用 LinkedHashMap 構建 LRU 緩存。
總結
其實 LinkedHashMap 幾乎和 HashMap 一樣:從技術上來說,不同的是它定義了一個 Entry<K,V> header,這個 header 不是放在 Table 里,它是額外獨立出來的。LinkedHashMap 通過繼承 hashMap 中的 Entry<K,V>,並添加兩個屬性 Entry<K,V> before,after,和 header 結合起來組成一個雙向鏈表,來實現按插入順序或訪問順序排序。
在寫關於 LinkedHashMap 的過程中,記起來之前面試的過程中遇到的一個問題,也是問我 Map 的哪種實現可以做到按照插入順序進行迭代?當時腦子是突然短路的,但現在想想,也只能怪自己對這個知識點還是掌握的不夠扎實,所以又從頭認真的把代碼看了一遍。
不過,我的建議是,大家首先首先需要記住的是:LinkedHashMap 能夠做到按照插入順序或者訪問順序進行迭代,這樣在我們以后的開發中遇到相似的問題,才能想到用 LinkedHashMap 來解決,否則就算對其內部結構非常了解,不去使用也是沒有什么用的。
LinkedHashSet 的實現原理
LinkedHashSet 概述
思考了好久,到底要不要總結 LinkedHashSet 的內容 = = 我在之前的博文中,分別寫了 HashMap 和 HashSet,然后我們可以看到 HashSet 的方法基本上都是基於 HashMap 來實現的,說白了,HashSet內部的數據結構就是一個 HashMap,其方法的內部幾乎就是在調用 HashMap 的方法。
LinkedHashSet 首先我們需要知道的是它是一個 Set 的實現,所以它其中存的肯定不是鍵值對,而是值。此實現與 HashSet 的不同之處在於,LinkedHashSet 維護着一個運行於所有條目的雙重鏈接列表。此鏈接列表定義了迭代順序,該迭代順序可為插入順序或是訪問順序。
看到上面的介紹,是不是感覺其與 HashMap 和 LinkedHashMap 的關系很像?
注意,此實現不是同步的。如果多個線程同時訪問鏈接的哈希Set,而其中至少一個線程修改了該 Set,則它必須保持外部同步。
小 Demo
在LinkedHashMap的實現原理中,通過例子演示了 HashMap 和 LinkedHashMap 的區別。舉一反三,我們現在學習的LinkedHashSet與之前的很相同,只不過之前存的是鍵值對,而現在存的只有值。
所以我就不再具體的貼代碼在這邊了,但我們可以肯定的是,LinkedHashSet 是可以按照插入順序或者訪問順序進行迭代。
LinkedHashSet 的實現
對於 LinkedHashSet 而言,它繼承與 HashSet、又基於 LinkedHashMap 來實現的。
LinkedHashSet 底層使用 LinkedHashMap 來保存所有元素,它繼承與 HashSet,其所有的方法操作上又與 HashSet 相同,因此 LinkedHashSet 的實現上非常簡單,只提供了四個構造方法,並通過傳遞一個標識參數,調用父類的構造器,底層構造一個 LinkedHashMap 來實現,在相關操作上與父類 HashSet 的操作相同,直接調用父類 HashSet 的方法即可。LinkedHashSet 的源代碼如下:
-
public class LinkedHashSet<E>
-
extends HashSet<E>
-
implements Set<E>, Cloneable, java.io.Serializable {
-
-
private static final long serialVersionUID = -2851667679971038690L;
-
-
/**
-
* 構造一個帶有指定初始容量和加載因子的新空鏈接哈希set。
-
*
-
* 底層會調用父類的構造方法,構造一個有指定初始容量和加載因子的LinkedHashMap實例。
-
* @param initialCapacity 初始容量。
-
* @param loadFactor 加載因子。
-
*/
-
public LinkedHashSet(int initialCapacity, float loadFactor) {
-
super(initialCapacity, loadFactor, true);
-
}
-
-
/**
-
* 構造一個帶指定初始容量和默認加載因子0.75的新空鏈接哈希set。
-
*
-
* 底層會調用父類的構造方法,構造一個帶指定初始容量和默認加載因子0.75的LinkedHashMap實例。
-
* @param initialCapacity 初始容量。
-
*/
-
public LinkedHashSet(int initialCapacity) {
-
super(initialCapacity, .75f, true);
-
}
-
-
/**
-
* 構造一個帶默認初始容量16和加載因子0.75的新空鏈接哈希set。
-
*
-
* 底層會調用父類的構造方法,構造一個帶默認初始容量16和加載因子0.75的LinkedHashMap實例。
-
*/
-
public LinkedHashSet() {
-
super(16, .75f, true);
-
}
-
-
/**
-
* 構造一個與指定collection中的元素相同的新鏈接哈希set。
-
*
-
* 底層會調用父類的構造方法,構造一個足以包含指定collection
-
* 中所有元素的初始容量和加載因子為0.75的LinkedHashMap實例。
-
* @param c 其中的元素將存放在此set中的collection。
-
*/
-
public LinkedHashSet(Collection<? extends E> c) {
-
super(Math.max(2*c.size(), 11), .75f, true);
-
addAll(c);
-
}
-
}
以上幾乎就是 LinkedHashSet 的全部代碼了,那么讀者可能就會懷疑了,不是說 LinkedHashSet 是基於 LinkedHashMap 實現的嗎?那我為什么在源碼中甚至都沒有看到出現過 LinkedHashMap。不要着急,我們可以看到在 LinkedHashSet 的構造方法中,其調用了父類的構造方法。我們可以進去看一下:
-
/**
-
* 以指定的initialCapacity和loadFactor構造一個新的空鏈接哈希集合。
-
* 此構造函數為包訪問權限,不對外公開,實際只是是對LinkedHashSet的支持。
-
*
-
* 實際底層會以指定的參數構造一個空LinkedHashMap實例來實現。
-
* @param initialCapacity 初始容量。
-
* @param loadFactor 加載因子。
-
* @param dummy 標記。
-
*/
-
HashSet( int initialCapacity, float loadFactor, boolean dummy) {
-
map = new LinkedHashMap<E,Object>(initialCapacity, loadFactor);
-
}
在父類 HashSet 中,專為 LinkedHashSet 提供的構造方法如下,該方法為包訪問權限,並未對外公開。
由上述源代碼可見,LinkedHashSet 通過繼承 HashSet,底層使用 LinkedHashMap,以很簡單明了的方式來實現了其自身的所有功能。
總結
以上就是關於 LinkedHashSet 的內容,我們只是從概述上以及構造方法這幾個方面介紹了,並不是我們不想去深入其讀取或者寫入方法,而是其本身沒有實現,只是繼承於父類 HashSet 的方法。
所以我們需要注意的點是:
- LinkedHashSet 是 Set 的一個具體實現,其維護着一個運行於所有條目的雙重鏈接列表。此鏈接列表定義了迭代順序,該迭代順序可為插入順序或是訪問順序。
- LinkedHashSet 繼承與 HashSet,並且其內部是通過 LinkedHashMap 來實現的。有點類似於我們之前說的LinkedHashMap 其內部是基於 Hashmap 實現一樣,不過還是有一點點區別的(具體的區別大家可以自己去思考一下)。
- 如果我們需要迭代的順序為插入順序或者訪問順序,那么 LinkedHashSet 是需要你首先考慮的。
ArrayList 的實現原理
ArrayList 概述
ArrayList 可以理解為動態數組,用 MSDN 中的說法,就是 Array 的復雜版本。與 Java 中的數組相比,它的容量能動態增長。ArrayList 是 List 接口的可變數組的實現。實現了所有可選列表操作,並允許包括 null 在內的所有元素。除了實現 List 接口外,此類還提供一些方法來操作內部用來存儲列表的數組的大小。(此類大致上等同於 Vector 類,除了此類是不同步的。)
每個 ArrayList 實例都有一個容量,該容量是指用來存儲列表元素的數組的大小。它總是至少等於列表的大小。隨着向 ArrayList 中不斷添加元素,其容量也自動增長。自動增長會帶來數據向新數組的重新拷貝,因此,如果可預知數據量的多少,可在構造 ArrayList 時指定其容量。在添加大量元素前,應用程序也可以使用 ensureCapacity 操作來增加 ArrayList 實例的容量,這可以減少遞增式再分配的數量。
注意,此實現不是同步的。如果多個線程同時訪問一個 ArrayList 實例,而其中至少一個線程從結構上修改了列表,那么它必須保持外部同步。(結構上的修改是指任何添加或刪除一個或多個元素的操作,或者顯式調整底層數組的大小;僅僅設置元素的值不是結構上的修改。)
我們先學習了解其內部的實現原理,才能更好的理解其應用。
ArrayList 的實現
對於 ArrayList 而言,它實現 List 接口、底層使用數組保存所有元素。其操作基本上是對數組的操作。下面我們來分析 ArrayList 的源代碼:
實現的接口
-
public class ArrayList<E> extends AbstractList<E>
-
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
-
{
-
}
ArrayList 繼承了 AbstractList,實現了 List。它是一個數組隊列,提供了相關的添加、刪除、修改、遍歷等功能。
ArrayList 實現了 RandmoAccess 接口,即提供了隨機訪問功能。RandmoAccess 是 java 中用來被 List 實現,為 List 提供快速訪問功能的。在 ArrayList 中,我們即可以通過元素的序號快速獲取元素對象;這就是快速隨機訪問。
ArrayList 實現了 Cloneable 接口,即覆蓋了函數 clone(),能被克隆。 ArrayList 實現 java.io.Serializable 接口,這意味着 ArrayList 支持序列化,能通過序列化去傳輸。
底層使用數組實現
-
/**
-
* The array buffer into which the elements of the ArrayList are stored.
-
* The capacity of the ArrayList is the length of this array buffer.
-
*/
-
private transient Object[] elementData;
構造方法
-
-
/**
-
* Constructs an empty list with an initial capacity of ten.
-
*/
-
public ArrayList() {
-
this(10);
-
}
-
/**
-
* Constructs an empty list with the specified initial capacity.
-
*
-
* @param initialCapacity the initial capacity of the list
-
* @throws IllegalArgumentException if the specified initial capacity
-
* is negative
-
*/
-
public ArrayList(int initialCapacity) {
-
super();
-
if (initialCapacity < 0)
-
throw new IllegalArgumentException("Illegal Capacity: "+
-
initialCapacity);
-
this.elementData = new Object[initialCapacity];
-
}
-
-
/**
-
* Constructs a list containing the elements of the specified
-
* collection, in the order they are returned by the collection's
-
* iterator.
-
*
-
* @param c the collection whose elements are to be placed into this list
-
* @throws NullPointerException if the specified collection is null
-
*/
-
public ArrayList(Collection<? extends E> c) {
-
elementData = c.toArray();
-
size = elementData.length;
-
// c.toArray might (incorrectly) not return Object[] (see 6260652)
-
if (elementData.getClass() != Object[].class)
-
elementData = Arrays.copyOf(elementData, size, Object[].class);
-
}
ArrayList 提供了三種方式的構造器:
public ArrayList()可以構造一個默認初始容量為10的空列表;public ArrayList(int initialCapacity)構造一個指定初始容量的空列表;public ArrayList(Collection<? extends E> c)構造一個包含指定 collection 的元素的列表,這些元素按照該collection的迭代器返回它們的順序排列的。
存儲
ArrayList 中提供了多種添加元素的方法,下面將一一進行講解:
1.set(int index, E element):該方法首先調用rangeCheck(index)來校驗 index 變量是否超出數組范圍,超出則拋出異常。而后,取出原 index 位置的值,並且將新的 element 放入 Index 位置,返回 oldValue。
-
/**
-
* Replaces the element at the specified position in this list with
-
* the specified element.
-
*
-
* @param index index of the element to replace
-
* @param element element to be stored at the specified position
-
* @return the element previously at the specified position
-
* @throws IndexOutOfBoundsException {@inheritDoc}
-
*/
-
public E set(int index, E element) {
-
rangeCheck(index);
-
-
E oldValue = elementData(index);
-
elementData[index] = element;
-
return oldValue;
-
}
-
/**
-
* Checks if the given index is in range. If not, throws an appropriate
-
* runtime exception. This method does *not* check if the index is
-
* negative: It is always used immediately prior to an array access,
-
* which throws an ArrayIndexOutOfBoundsException if index is negative.
-
*/
-
private void rangeCheck(int index) {
-
if (index >= size)
-
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
-
}
2.add(E e):該方法是將指定的元素添加到列表的尾部。當容量不足時,會調用 grow 方法增長容量。
-
/**
-
* Appends the specified element to the end of this list.
-
*
-
* @param e element to be appended to this list
-
* @return <tt>true</tt> (as specified by {@link Collection#add})
-
*/
-
public boolean add(E e) {
-
ensureCapacityInternal(size + 1); // Increments modCount!!
-
elementData[size++] = e;
-
return true;
-
}
-
private void ensureCapacityInternal(int minCapacity) {
-
modCount++;
-
// overflow-conscious code
-
if (minCapacity - elementData.length > 0)
-
grow(minCapacity);
-
}
-
private void grow(int minCapacity) {
-
// overflow-conscious code
-
int oldCapacity = elementData.length;
-
int newCapacity = oldCapacity + (oldCapacity >> 1);
-
if (newCapacity - minCapacity < 0)
-
newCapacity = minCapacity;
-
if (newCapacity - MAX_ARRAY_SIZE > 0)
-
newCapacity = hugeCapacity(minCapacity);
-
// minCapacity is usually close to size, so this is a win:
-
elementData = Arrays.copyOf(elementData, newCapacity);
-
}
3.add(int index, E element):在 index 位置插入 element。
-
/**
-
* Inserts the specified element at the specified position in this
-
* list. Shifts the element currently at that position (if any) and
-
* any subsequent elements to the right (adds one to their indices).
-
*
-
* @param index index at which the specified element is to be inserted
-
* @param element element to be inserted
-
* @throws IndexOutOfBoundsException {@inheritDoc}
-
*/
-
public void add(int index, E element) {
-
rangeCheckForAdd(index);
-
-
ensureCapacityInternal(size + 1); // Increments modCount!!
-
System.arraycopy(elementData, index, elementData, index + 1,
-
size - index);
-
elementData[index] = element;
-
size++;
-
}
4.addAll(Collection<? extends E> c) 和 addAll(int index, Collection<? extends E> c):將特定 Collection 中的元素添加到 Arraylist 末尾。
-
/**
-
* Appends all of the elements in the specified collection to the end of
-
* this list, in the order that they are returned by the
-
* specified collection's Iterator. The behavior of this operation is
-
* undefined if the specified collection is modified while the operation
-
* is in progress. (This implies that the behavior of this call is
-
* undefined if the specified collection is this list, and this
-
* list is nonempty.)
-
*
-
* @param c collection containing elements to be added to this list
-
* @return <tt>true</tt> if this list changed as a result of the call
-
* @throws NullPointerException if the specified collection is null
-
*/
-
public boolean addAll(Collection<? extends E> c) {
-
Object[] a = c.toArray();
-
int numNew = a.length;
-
ensureCapacityInternal(size + numNew); // Increments modCount
-
System.arraycopy(a, 0, elementData, size, numNew);
-
size += numNew;
-
return numNew != 0;
-
}
-
-
/**
-
* Inserts all of the elements in the specified collection into this
-
* list, starting at the specified position. Shifts the element
-
* currently at that position (if any) and any subsequent elements to
-
* the right (increases their indices). The new elements will appear
-
* in the list in the order that they are returned by the
-
* specified collection's iterator.
-
*
-
* @param index index at which to insert the first element from the
-
* specified collection
-
* @param c collection containing elements to be added to this list
-
* @return <tt>true</tt> if this list changed as a result of the call
-
* @throws IndexOutOfBoundsException {@inheritDoc}
-
* @throws NullPointerException if the specified collection is null
-
*/
-
public boolean addAll(int index, Collection<? extends E> c) {
-
rangeCheckForAdd(index);
-
-
Object[] a = c.toArray();
-
int numNew = a.length;
-
ensureCapacityInternal(size + numNew); // Increments modCount
-
-
int numMoved = size - index;
-
if (numMoved > 0)
-
System.arraycopy(elementData, index, elementData, index + numNew,
-
numMoved);
-
-
System.arraycopy(a, 0, elementData, index, numNew);
-
size += numNew;
-
return numNew != 0;
-
}
在 ArrayList 的存儲方法,其核心本質是在數組的某個位置將元素添加進入。但其中又會涉及到關於數組容量不夠而增長等因素。
讀取
這個方法就比較簡單了,ArrayList 能夠支持隨機訪問的原因也是很顯然的,因為它內部的數據結構是數組,而數組本身就是支持隨機訪問。該方法首先會判斷輸入的index值是否越界,然后將數組的 index 位置的元素返回即可。
-
/**
-
* Returns the element at the specified position in this list.
-
*
-
* @param index index of the element to return
-
* @return the element at the specified position in this list
-
* @throws IndexOutOfBoundsException {@inheritDoc}
-
*/
-
public E get(int index) {
-
rangeCheck(index);
-
return (E) elementData[index];
-
}
-
private void rangeCheck(int index) {
-
if (index >= size)
-
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
-
}
刪除
ArrayList 提供了根據下標或者指定對象兩種方式的刪除功能。需要注意的是該方法的返回值並不相同,如下:
-
/**
-
* Removes the element at the specified position in this list.
-
* Shifts any subsequent elements to the left (subtracts one from their
-
* indices).
-
*
-
* @param index the index of the element to be removed
-
* @return the element that was removed from the list
-
* @throws IndexOutOfBoundsException {@inheritDoc}
-
*/
-
public E remove(int index) {
-
rangeCheck(index);
-
-
modCount++;
-
E oldValue = elementData(index);
-
-
int numMoved = size - index - 1;
-
if (numMoved > 0)
-
System.arraycopy(elementData, index+ 1, elementData, index,
-
numMoved);
-
elementData[--size] = null; // Let gc do its work
-
-
return oldValue;
-
}
-
/**
-
* Removes the first occurrence of the specified element from this list,
-
* if it is present. If the list does not contain the element, it is
-
* unchanged. More formally, removes the element with the lowest index
-
* <tt>i</tt> such that
-
* <tt>(o==null ? get(i)==null : o.equals(get(i)))</tt>
-
* (if such an element exists). Returns <tt>true</tt> if this list
-
* contained the specified element (or equivalently, if this list
-
* changed as a result of the call).
-
*
-
* @param o element to be removed from this list, if present
-
* @return <tt>true</tt> if this list contained the specified element
-
*/
-
public boolean remove(Object o) {
-
if (o == null) {
-
for (int index = 0; index < size; index++)
-
if (elementData[index] == null) {
-
fastRemove(index);
-
return true;
-
}
-
} else {
-
for (int index = 0; index < size; index++)
-
if (o.equals(elementData[index])) {
-
fastRemove(index);
-
return true;
-
}
-
}
-
return false;
-
}
注意:從數組中移除元素的操作,也會導致被移除的元素以后的所有元素的向左移動一個位置。
調整數組容量
從上面介紹的向 ArrayList 中存儲元素的代碼中,我們看到,每當向數組中添加元素時,都要去檢查添加后元素的個數是否會超出當前數組的長度,如果超出,數組將會進行擴容,以滿足添加數據的需求。數組擴容有兩個方法,其中開發者可以通過一個 public 的方法ensureCapacity(int minCapacity)來增加 ArrayList 的容量,而在存儲元素等操作過程中,如果遇到容量不足,會調用priavte方法private void ensureCapacityInternal(int minCapacity)實現。
-
public void ensureCapacity(int minCapacity) {
-
if (minCapacity > 0)
-
ensureCapacityInternal(minCapacity);
-
}
-
-
private void ensureCapacityInternal(int minCapacity) {
-
modCount++;
-
// overflow-conscious code
-
if (minCapacity - elementData.length > 0)
-
grow(minCapacity);
-
}
-
/**
-
* Increases the capacity to ensure that it can hold at least the
-
* number of elements specified by the minimum capacity argument.
-
*
-
* @param minCapacity the desired minimum capacity
-
*/
-
private void grow(int minCapacity) {
-
// overflow-conscious code
-
int oldCapacity = elementData.length;
-
int newCapacity = oldCapacity + (oldCapacity >> 1);
-
if (newCapacity - minCapacity < 0)
-
newCapacity = minCapacity;
-
if (newCapacity - MAX_ARRAY_SIZE > 0)
-
newCapacity = hugeCapacity(minCapacity);
-
// minCapacity is usually close to size, so this is a win:
-
elementData = Arrays.copyOf(elementData, newCapacity);
-
}
從上述代碼中可以看出,數組進行擴容時,會將老數組中的元素重新拷貝一份到新的數組中,每次數組容量的增長大約是其原容量的 1.5 倍(從int newCapacity = oldCapacity + (oldCapacity >> 1)這行代碼得出)。這種操作的代價是很高的,因此在實際使用時,我們應該盡量避免數組容量的擴張。當我們可預知要保存的元素的多少時,要在構造 ArrayList 實例時,就指定其容量,以避免數組擴容的發生。或者根據實際需求,通過調用ensureCapacity 方法來手動增加 ArrayList 實例的容量。
Fail-Fast 機制
ArrayList 也采用了快速失敗的機制,通過記錄 modCount 參數來實現。在面對並發的修改時,迭代器很快就會完全失敗,而不是冒着在將來某個不確定時間發生任意不確定行為的風險。 關於 Fail-Fast 的更詳細的介紹,我在之前將 HashMap 中已經提到。
LinkedList 的實現原理
概述
LinkedList 和 ArrayList 一樣,都實現了 List 接口,但其內部的數據結構有本質的不同。LinkedList 是基於鏈表實現的(通過名字也能區分開來),所以它的插入和刪除操作比 ArrayList 更加高效。但也是由於其為基於鏈表的,所以隨機訪問的效率要比 ArrayList 差。
看一下 LinkedList 的類的定義:
-
public class LinkedList<E>
-
extends AbstractSequentialList<E>
-
implements List<E>, Deque<E>, Cloneable, java.io.Serializable
-
{}
LinkedList 繼承自 AbstractSequenceList,實現了 List、Deque、Cloneable、java.io.Serializable 接口。AbstractSequenceList 提供了List接口骨干性的實現以減少實現 List 接口的復雜度,Deque 接口定義了雙端隊列的操作。
在 LinkedList 中除了本身自己的方法外,還提供了一些可以使其作為棧、隊列或者雙端隊列的方法。這些方法可能彼此之間只是名字不同,以使得這些名字在特定的環境中顯得更加合適。
LinkedList 也是 fail-fast 的(前邊提過很多次了)。
LinkedList 源碼解讀
數據結構
LinkedList 是基於鏈表結構實現,所以在類中包含了 first 和 last 兩個指針(Node)。Node 中包含了上一個節點和下一個節點的引用,這樣就構成了雙向的鏈表。每個 Node 只能知道自己的前一個節點和后一個節點,但對於鏈表來說,這已經足夠了。
-
transient int size = 0;
-
transient Node<E> first; //鏈表的頭指針
-
transient Node<E> last; //尾指針
-
//存儲對象的結構 Node, 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;
-
}
-
}
存儲
add(E e)
該方法是在鏈表的 end 添加元素,其調用了自己的方法 linkLast(E e)。
該方法首先將 last 的 Node 引用指向了一個新的 Node(l),然后根據l新建了一個 newNode,其中的元素就為要添加的 e;而后,我們讓 last 指向了 newNode。接下來是自身進行維護該鏈表。
-
/**
-
* Appends the specified element to the end of this list.
-
*
-
* <p>This method is equivalent to {@link #addLast}.
-
*
-
* @param e element to be appended to this list
-
* @return {@code true} (as specified by {@link Collection#add})
-
*/
-
public boolean add(E e) {
-
linkLast(e);
-
return true;
-
}
-
/**
-
* Links e as last element.
-
*/
-
void linkLast(E e) {
-
final Node<E> l = last;
-
final Node<E> newNode = new Node<>(l, e, null);
-
last = newNode;
-
if (l == null)
-
first = newNode;
-
else
-
l.next = newNode;
-
size++;
-
modCount++;
-
}
add(int index, E element)
該方法是在指定 index 位置插入元素。如果 index 位置正好等於 size,則調用 linkLast(element) 將其插入末尾;否則調用 linkBefore(element, node(index))方法進行插入。該方法的實現在下面,大家可以自己仔細的分析一下。(分析鏈表的時候最好能夠邊畫圖邊分析)
-
/**
-
* Inserts the specified element at the specified position in this list.
-
* Shifts the element currently at that position (if any) and any
-
* subsequent elements to the right (adds one to their indices).
-
*
-
* @param index index at which the specified element is to be inserted
-
* @param element element to be inserted
-
* @throws IndexOutOfBoundsException {@inheritDoc}
-
*/
-
public void add(int index, E element) {
-
checkPositionIndex(index);
-
-
if (index == size)
-
linkLast(element);
-
else
-
linkBefore(element, node(index));
-
}
-
/**
-
* Inserts element e before non-null Node succ.
-
*/
-
void linkBefore(E e, Node<E> succ) {
-
// assert succ != null;
-
final Node<E> pred = succ.prev;
-
final Node<E> newNode = new Node<>(pred, e, succ);
-
succ.prev = newNode;
-
if (pred == null)
-
first = newNode;
-
else
-
pred.next = newNode;
-
size++;
-
modCount++;
-
}
LinkedList 的方法實在是太多,在這沒法一一舉例分析。但很多方法其實都只是在調用別的方法而已,所以建議大家將其幾個最核心的添加的方法搞懂就可以了,比如 linkBefore、linkLast。其本質也就是鏈表之間的刪除添加等。
ConcurrentHashMap 的實現原理
概述
我們在之前的博文中了解到關於 HashMap 和 Hashtable 這兩種集合。其中 HashMap 是非線程安全的,當我們只有一個線程在使用 HashMap 的時候,自然不會有問題,但如果涉及到多個線程,並且有讀有寫的過程中,HashMap 就不能滿足我們的需要了(fail-fast)。在不考慮性能問題的時候,我們的解決方案有 Hashtable 或者Collections.synchronizedMap(hashMap),這兩種方式基本都是對整個 hash 表結構做鎖定操作的,這樣在鎖表的期間,別的線程就需要等待了,無疑性能不高。
所以我們在本文中學習一個 util.concurrent 包的重要成員,ConcurrentHashMap。
ConcurrentHashMap 的實現是依賴於 Java 內存模型,所以我們在了解 ConcurrentHashMap 的前提是必須了解Java 內存模型。但 Java 內存模型並不是本文的重點,所以我假設讀者已經對 Java 內存模型有所了解。
ConcurrentHashMap 分析
ConcurrentHashMap 的結構是比較復雜的,都深究去本質,其實也就是數組和鏈表而已。我們由淺入深慢慢的分析其結構。
先簡單分析一下,ConcurrentHashMap 的成員變量中,包含了一個 Segment 的數組(final Segment<K,V>[] segments;),而 Segment 是 ConcurrentHashMap 的內部類,然后在 Segment 這個類中,包含了一個 HashEntry 的數組(transient volatile HashEntry<K,V>[] table;)。而 HashEntry 也是 ConcurrentHashMap 的內部類。HashEntry 中,包含了 key 和 value 以及 next 指針(類似於 HashMap 中 Entry),所以 HashEntry 可以構成一個鏈表。
所以通俗的講,ConcurrentHashMap 數據結構為一個 Segment 數組,Segment 的數據結構為 HashEntry 的數組,而 HashEntry 存的是我們的鍵值對,可以構成鏈表。
首先,我們看一下 HashEntry 類。
HashEntry
HashEntry 用來封裝散列映射表中的鍵值對。在 HashEntry 類中,key,hash 和 next 域都被聲明為 final 型,value 域被聲明為 volatile 型。其類的定義為:
-
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;
-
}
-
...
-
...
-
}
HashEntry 的學習可以類比着 HashMap 中的 Entry。我們的存儲鍵值對的過程中,散列的時候如果發生“碰撞”,將采用“分離鏈表法”來處理碰撞:把碰撞的 HashEntry 對象鏈接成一個鏈表。
如下圖,我們在一個空桶中插入 A、B、C 兩個 HashEntry 對象后的結構圖(其實應該為鍵值對,在這進行了簡化以方便更容易理解):
Segment
Segment 的類定義為static final class Segment<K,V> extends ReentrantLock implements Serializable。其繼承於 ReentrantLock 類,從而使得 Segment 對象可以充當鎖的角色。Segment 中包含HashEntry 的數組,其可以守護其包含的若干個桶(HashEntry的數組)。Segment 在某些意義上有點類似於 HashMap了,都是包含了一個數組,而數組中的元素可以是一個鏈表。
table:table 是由 HashEntry 對象組成的數組如果散列時發生碰撞,碰撞的 HashEntry 對象就以鏈表的形式鏈接成一個鏈表table數組的數組成員代表散列映射表的一個桶每個 table 守護整個 ConcurrentHashMap 包含桶總數的一部分如果並發級別為 16,table 則守護 ConcurrentHashMap 包含的桶總數的 1/16。
count 變量是計算器,表示每個 Segment 對象管理的 table 數組(若干個 HashEntry 的鏈表)包含的HashEntry 對象的個數。之所以在每個Segment對象中包含一個 count 計數器,而不在 ConcurrentHashMap 中使用全局的計數器,是為了避免出現“熱點域”而影響並發性。
-
/**
-
* Segments are specialized versions of hash tables. This
-
* subclasses from ReentrantLock opportunistically, just to
-
* simplify some locking and avoid separate construction.
-
*/
-
static final class Segment<K,V> extends ReentrantLock implements Serializable {
-
/**
-
* The per-segment table. Elements are accessed via
-
* entryAt/setEntryAt providing volatile semantics.
-
*/
-
transient volatile HashEntry<K,V>[] table;
-
-
/**
-
* The number of elements. Accessed only either within locks
-
* or among other volatile reads that maintain visibility.
-
*/
-
transient int count;
-
transient int modCount;
-
/**
-
* 裝載因子
-
*/
-
final float loadFactor;
-
}
我們通過下圖來展示一下插入 ABC 三個節點后,Segment 的示意圖:
其實從我個人角度來說,Segment結構是與HashMap很像的。
ConcurrentHashMap
ConcurrentHashMap 的結構中包含的 Segment 的數組,在默認的並發級別會創建包含 16 個 Segment 對象的數組。通過我們上面的知識,我們知道每個 Segment 又包含若干個散列表的桶,每個桶是由 HashEntry 鏈接起來的一個鏈表。如果 key 能夠均勻散列,每個 Segment 大約守護整個散列表桶總數的 1/16。
下面我們還有通過一個圖來演示一下 ConcurrentHashMap 的結構:
並發寫操作
在 ConcurrentHashMap 中,當執行 put 方法的時候,會需要加鎖來完成。我們通過代碼來解釋一下具體過程: 當我們 new 一個 ConcurrentHashMap 對象,並且執行put操作的時候,首先會執行 ConcurrentHashMap 類中的 put 方法,該方法源碼為:
-
/**
-
* Maps the specified key to the specified value in this table.
-
* Neither the key nor the value can be null.
-
*
-
* <p> The value can be retrieved by calling the <tt>get</tt> method
-
* with a key that is equal to the original key.
-
*
-
* @param key key with which the specified value is to be associated
-
* @param value value to be associated with the specified key
-
* @return the previous value associated with <tt>key</tt>, or
-
* <tt>null</tt> if there was no mapping for <tt>key</tt>
-
* @throws NullPointerException if the specified key or value is null
-
*/
-
-
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 // nonvolatile; recheck
-
(segments, (j << SSHIFT) + SBASE)) == null) // in ensureSegment
-
s = ensureSegment(j);
-
return s.put(key, hash, value, false);
-
}
我們通過注釋可以了解到,ConcurrentHashMap 不允許空值。該方法首先有一個 Segment 的引用 s,然后會通過 hash() 方法對 key 進行計算,得到哈希值;繼而通過調用 Segment 的 put(K key, int hash, V value, boolean onlyIfAbsent)方法進行存儲操作。該方法源碼為:
-
final V put(K key, int hash, V value, boolean onlyIfAbsent) {
-
//加鎖,這里是鎖定的Segment而不是整個ConcurrentHashMap
-
HashEntry<K,V> node = tryLock() ? null :scanAndLockForPut(key, hash, value);
-
V oldValue;
-
try {
-
HashEntry<K,V>[] tab = table;
-
//得到hash對應的table中的索引index
-
int index = (tab.length - 1) & hash;
-
//找到hash對應的是具體的哪個桶,也就是哪個HashEntry鏈表
-
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;
-
}
關於該方法的某些關鍵步驟,在源碼上加上了注釋。
需要注意的是:加鎖操作是針對的 hash 值對應的某個 Segment,而不是整個 ConcurrentHashMap。因為 put 操作只是在這個 Segment 中完成,所以並不需要對整個 ConcurrentHashMap 加鎖。所以,此時,其他的線程也可以對另外的 Segment 進行 put 操作,因為雖然該 Segment 被鎖住了,但其他的 Segment 並沒有加鎖。同時,讀線程並不會因為本線程的加鎖而阻塞。
正是因為其內部的結構以及機制,所以 ConcurrentHashMap 在並發訪問的性能上要比Hashtable和同步包裝之后的HashMap的性能提高很多。在理想狀態下,ConcurrentHashMap 可以支持 16 個線程執行並發寫操作(如果並發級別設置為 16),及任意數量線程的讀操作。
總結
在實際的應用中,散列表一般的應用場景是:除了少數插入操作和刪除操作外,絕大多數都是讀取操作,而且讀操作在大多數時候都是成功的。正是基於這個前提,ConcurrentHashMap 針對讀操作做了大量的優化。通過 HashEntry 對象的不變性和用 volatile 型變量協調線程間的內存可見性,使得 大多數時候,讀操作不需要加鎖就可以正確獲得值。這個特性使得 ConcurrentHashMap 的並發性能在分離鎖的基礎上又有了近一步的提高。
ConcurrentHashMap 是一個並發散列映射表的實現,它允許完全並發的讀取,並且支持給定數量的並發更新。相比於 HashTable 和用同步包裝器包裝的 HashMap(Collections.synchronizedMap(new HashMap())),ConcurrentHashMap 擁有更高的並發性。在 HashTable 和由同步包裝器包裝的 HashMap 中,使用一個全局的鎖來同步不同線程間的並發訪問。同一時間點,只能有一個線程持有鎖,也就是說在同一時間點,只能有一個線程能訪問容器。這雖然保證多線程間的安全並發訪問,但同時也導致對容器的訪問變成串行化的了。
ConcurrentHashMap 的高並發性主要來自於三個方面:
- 用分離鎖實現多個線程間的更深層次的共享訪問。
- 用 HashEntery 對象的不變性來降低執行讀操作的線程在遍歷鏈表期間對加鎖的需求。
- 通過對同一個 Volatile 變量的寫 / 讀訪問,協調不同線程間讀 / 寫操作的內存可見性。
使用分離鎖,減小了請求 同一個鎖的頻率。
通過 HashEntery 對象的不變性及對同一個 Volatile 變量的讀 / 寫來協調內存可見性,使得 讀操作大多數時候不需要加鎖就能成功獲取到需要的值。由於散列映射表在實際應用中大多數操作都是成功的 讀操作,所以 2 和 3 既可以減少請求同一個鎖的頻率,也可以有效減少持有鎖的時間。通過減小請求同一個鎖的頻率和盡量減少持有鎖的時間 ,使得 ConcurrentHashMap 的並發性相對於 HashTable 和用同步包裝器包裝的 HashMap有了質的提高。
LinkedHashMap 與 LRUcache
LRU 緩存介紹
我們平時總會有一個電話本記錄所有朋友的電話,但是,如果有朋友經常聯系,那些朋友的電話號碼不用翻電話本我們也能記住,但是,如果長時間沒有聯系了,要再次聯系那位朋友的時候,我們又不得不求助電話本,但是,通過電話本查找還是很費時間的。但是,我們大腦能夠記住的東西是一定的,我們只能記住自己最熟悉的,而長時間不熟悉的自然就忘記了。
其實,計算機也用到了同樣的一個概念,我們用緩存來存放以前讀取的數據,而不是直接丟掉,這樣,再次讀取的時候,可以直接在緩存里面取,而不用再重新查找一遍,這樣系統的反應能力會有很大提高。但是,當我們讀取的個數特別大的時候,我們不可能把所有已經讀取的數據都放在緩存里,畢竟內存大小是一定的,我們一般把最近常讀取的放在緩存里(相當於我們把最近聯系的朋友的姓名和電話放在大腦里一樣)。
LRU 緩存利用了這樣的一種思想。LRU 是 Least Recently Used 的縮寫,翻譯過來就是“最近最少使用”,也就是說,LRU 緩存把最近最少使用的數據移除,讓給最新讀取的數據。而往往最常讀取的,也是讀取次數最多的,所以,利用 LRU 緩存,我們能夠提高系統的 performance。
實現
要實現 LRU 緩存,我們首先要用到一個類 LinkedHashMap。
用這個類有兩大好處:一是它本身已經實現了按照訪問順序的存儲,也就是說,最近讀取的會放在最前面,最最不常讀取的會放在最后(當然,它也可以實現按照插入順序存儲)。第二,LinkedHashMap 本身有一個方法用於判斷是否需要移除最不常讀取的數,但是,原始方法默認不需要移除(這是,LinkedHashMap 相當於一個linkedlist),所以,我們需要 override 這樣一個方法,使得當緩存里存放的數據個數超過規定個數后,就把最不常用的移除掉。關於 LinkedHashMap 中已經有詳細的介紹。
代碼如下:(可直接復制,也可以通過LRUcache-Java下載)
-
import java.util.LinkedHashMap;
-
import java.util.Collection;
-
import java.util.Map;
-
import java.util.ArrayList;
-
-
/**
-
* An LRU cache, based on <code>LinkedHashMap</code>.
-
*
-
* <p>
-
* This cache has a fixed maximum number of elements (<code>cacheSize</code>).
-
* If the cache is full and another entry is added, the LRU (least recently
-
* used) entry is dropped.
-
*
-
* <p>
-
* This class is thread-safe. All methods of this class are synchronized.
-
*
-
* <p>
-
* Author: Christian d'Heureuse, Inventec Informatik AG, Zurich, Switzerland<br>
-
* Multi-licensed: EPL / LGPL / GPL / AL / BSD.
-
*/
-
public class LRUCache<K, V> {
-
private static final float hashTableLoadFactor = 0.75f;
-
private LinkedHashMap<K, V> map;
-
private int cacheSize;
-
-
/**
-
* Creates a new LRU cache. 在該方法中,new LinkedHashMap<K,V>(hashTableCapacity,
-
* hashTableLoadFactor, true)中,true代表使用訪問順序
-
*
-
* @param cacheSize
-
* the maximum number of entries that will be kept in this cache.
-
*/
-
public LRUCache(int cacheSize) {
-
this.cacheSize = cacheSize;
-
int hashTableCapacity = (int) Math
-
.ceil(cacheSize / hashTableLoadFactor) + 1;
-
map = new LinkedHashMap<K, V>(hashTableCapacity, hashTableLoadFactor,
-
true) {
-
// (an anonymous inner class)
-
private static final long serialVersionUID = 1;
-
-
-
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
-
return size() > LRUCache.this.cacheSize;
-
}
-
};
-
}
-
-
/**
-
* Retrieves an entry from the cache.<br>
-
* The retrieved entry becomes the MRU (most recently used) entry.
-
*
-
* @param key
-
* the key whose associated value is to be returned.
-
* @return the value associated to this key, or null if no value with this
-
* key exists in the cache.
-
*/
-
public synchronized V get(K key) {
-
return map.get(key);
-
}
-
-
/**
-
* Adds an entry to this cache. The new entry becomes the MRU (most recently
-
* used) entry. If an entry with the specified key already exists in the
-
* cache, it is replaced by the new entry. If the cache is full, the LRU
-
* (least recently used) entry is removed from the cache.
-
*
-
* @param key
-
* the key with which the specified value is to be associated.
-
* @param value
-
* a value to be associated with the specified key.
-
*/
-
public synchronized void put(K key, V value) {
-
map.put(key, value);
-
}
-
-
/**
-
* Clears the cache.
-
*/
-
public synchronized void clear() {
-
map.clear();
-
}
-
-
/**
-
* Returns the number of used entries in the cache.
-
*
-
* @return the number of entries currently in the cache.
-
*/
-
public synchronized int usedEntries() {
-
return map.size();
-
}
-
-
/**
-
* Returns a <code>Collection</code> that contains a copy of all cache
-
* entries.
-
*
-
* @return a <code>Collection</code> with a copy of the cache content.
-
*/
-
public synchronized Collection<Map.Entry<K, V>> getAll() {
-
return new ArrayList<Map.Entry<K, V>>(map.entrySet());
-
}
-
-
// Test routine for the LRUCache class.
-
public static void main(String[] args) {
-
LRUCache<String, String> c = new LRUCache<String, String>(3);
-
c.put( "1", "one"); // 1
-
c.put( "2", "two"); // 2 1
-
c.put( "3", "three"); // 3 2 1
-
c.put( "4", "four"); // 4 3 2
-
if (c.get("2") == null)
-
throw new Error(); // 2 4 3
-
c.put( "5", "five"); // 5 2 4
-
c.put( "4", "second four"); // 4 5 2
-
// Verify cache content.
-
if (c.usedEntries() != 3)
-
throw new Error();
-
if (!c.get("4").equals("second four"))
-
throw new Error();
-
if (!c.get("5").equals("five"))
-
throw new Error();
-
if (!c.get("2").equals("two"))
-
throw new Error();
-
// List cache content.
-
for (Map.Entry<String, String> e : c.getAll())
-
System.out.println(e.getKey() + " : " + e.getValue());
-
}
-
}
HashSet 和 HashMap 的比較
HashMap 和 HashSet 都是 collection 框架的一部分,它們讓我們能夠使用對象的集合。collection 框架有自己的接口和實現,主要分為 Set 接口,List 接口和 Queue 接口。它們有各自的特點,Set 的集合里不允許對象有重復的值,List 允許有重復,它對集合中的對象進行索引,Queue 的工作原理是 FCFS 算法(First Come, First Serve)。
首先讓我們來看看什么是 HashMap 和 HashSet,然后再來比較它們之間的分別。
什么是 HashSet
HashSet 實現了 Set 接口,它不允許集合中有重復的值,當我們提到 HashSet 時,第一件事情就是在將對象存儲在 HashSet 之前,要先確保對象重寫 equals()和 hashCode()方法,這樣才能比較對象的值是否相等,以確保set中沒有儲存相等的對象。如果我們沒有重寫這兩個方法,將會使用這個方法的默認實現。
public boolean add(Object o)方法用來在 Set 中添加元素,當元素值重復時則會立即返回 false,如果成功添加的話會返回 true。
什么是 HashMap
HashMap 實現了 Map 接口,Map 接口對鍵值對進行映射。Map 中不允許重復的鍵。Map 接口有兩個基本的實現,HashMap 和 TreeMap。TreeMap 保存了對象的排列次序,而 HashMap 則不能。HashMap 允許鍵和值為 null。HashMap 是非 synchronized 的,但 collection 框架提供方法能保證 HashMap synchronized,這樣多個線程同時訪問 HashMap 時,能保證只有一個線程更改 Map。
public Object put(Object Key,Object value)方法用來將元素添加到 map 中。
HashSet 和 HashMap 的區別
| HashMap | HashSet |
|---|---|
| HashMap實現了Map接口 | HashSet實現了Set接口 |
| HashMap儲存鍵值對 | HashSet僅僅存儲對象 |
| 使用put()方法將元素放入map中 | 使用add()方法將元素放入set中 |
| HashMap中使用鍵對象來計算hashcode值 | HashSet使用成員對象來計算hashcode值,對於兩個對象來說hashcode可能相同,所以equals()方法用來判斷對象的相等性,如果兩個對象不同的話,那么返回false |
| HashMap比較快,因為是使用唯一的鍵來獲取對象 | HashSet較HashMap來說比較慢 |
ct o)方法用來在 Set 中添加元素,當元素值重復時則會立即返回 false,如果成功添加的話會返回 true。
什么是 HashMap
HashMap 實現了 Map 接口,Map 接口對鍵值對進行映射。Map 中不允許重復的鍵。Map 接口有兩個基本的實現,HashMap 和 TreeMap。TreeMap 保存了對象的排列次序,而 HashMap 則不能。HashMap 允許鍵和值為 null。HashMap 是非 synchronized 的,但 collection 框架提供方法能保證 HashMap synchronized,這樣多個線程同時訪問 HashMap 時,能保證只有一個線程更改 Map。
public Object put(Object Key,Object value)方法用來將元素添加到 map 中。
HashSet 和 HashMap 的區別
| HashMap | HashSet |
|---|---|
| HashMap實現了Map接口 | HashSet實現了Set接口 |
| HashMap儲存鍵值對 | HashSet僅僅存儲對象 |
| 使用put()方法將元素放入map中 | 使用add()方法將元素放入set中 |
| HashMap中使用鍵對象來計算hashcode值 | HashSet使用成員對象來計算hashcode值,對於兩個對象來說hashcode可能相同,所以equals()方法用來判斷對象的相等性,如果兩個對象不同的話,那么返回false |
| HashMap比較快,因為是使用唯一的鍵來獲取對象 | HashSet較HashMap來說比較慢 |
