HashMap 是基於哈希表的 Map 接口的非同步實現。此實現提供全部可選的映射操做,並容許使用 null 值和 null 鍵。此類不保證映射的順序,特別是它不保證該順序恆久不變。java
此實現假定哈希函數將元素適當地分佈在各桶之間,可爲基本操做(get 和 put)提供穩定的性能。迭代 collection 視圖所需的時間與 HashMap 實例的「容量」(桶的數量)及其大小(鍵-值映射關係數)成比例。因此,若是迭代性能很重要,則不要將初始容量設置得過高或將加載因子設置得過低。也許你們開始對這段話有一點不太懂,不過不用擔憂,當你讀完這篇文章後,就能深切理解這其中的含義了。node
須要注意的是:Hashmap 不是同步的,若是多個線程同時訪問一個 HashMap,而其中至少一個線程從結構上(指添加或者刪除一個或多個映射關係的任何操做)修改了,則必須保持外部同步,以防止對映射進行意外的非同步訪問。git
在 Java 編程語言中,最基本的結構就是兩種,一個是數組,另一個是指針(引用),HashMap 就是經過這兩個數據結構進行實現。HashMap其實是一個「鏈表散列」的數據結構,即數組和鏈表的結合體。github
從上圖中能夠看出,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 對,它持有一個指向下一個元素的引用,這就構成了鏈表。數組
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 提供的一個包訪問權限的方法,代碼以下:
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) 方法的代碼以下:
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 中的元素愈來愈多的時候,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(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 容量是容量的兩倍:
咱們知道 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 包下的類」。
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 而言,它是基於 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 而言,它是基於 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); }
/** * @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 同樣,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是經過"拉鍊法"實現的哈希表。它包括幾個重要的成員變量:table, count, threshold, loadFactor, modCount。
關於變量的解釋在源碼註釋中都有,最好仍是應該看英文註釋。
/** * 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 方法的整個流程爲:
我在下面的代碼中也進行了一些註釋:
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,因此將該節點插入鏈表的第一個位置。
相比較於 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 有多種遍歷方式:
//一、使用keys() Enumeration<String> en1 = table.keys(); while(en1.hasMoreElements()) { en1.nextElement(); } //二、使用elements() Enumeration<String> en2 = table.elements(); while(en2.hasMoreElements()) { en2.nextElement(); } //三、使用keySet() Iterator<String> it1 = table.keySet().iterator(); while(it1.hasNext()) { it1.next(); } //四、使用entrySet() Iterator<Entry<String, String>> it2 = table.entrySet().iterator(); while(it2.hasNext()) { it2.next(); }
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方法後,會將此次訪問的元素移至鏈表尾部,不斷訪問能夠造成按訪問順序排序的鏈表。
我在最開始學習 LinkedHashMap 的時候,看到訪問順序、插入順序等等,有點暈了,隨着後續的學習才慢慢懂得其中原理,因此我會先在進行作幾個 demo 來演示一下 LinkedHashMap 的使用。看懂了其效果,而後再來研究其原理。
看下面這個代碼:
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=西瓜
咱們如今將 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 而言,它繼承與 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. */ @Override 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; }
public 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 的內容 = = 我在以前的博文中,分別寫了 HashMap 和 HashSet,而後咱們能夠看到 HashSet 的方法基本上都是基於 HashMap 來實現的,說白了,HashSet內部的數據結構就是一個 HashMap,其方法的內部幾乎就是在調用 HashMap 的方法。
LinkedHashSet 首先咱們須要知道的是它是一個 Set 的實現,因此它其中存的確定不是鍵值對,而是值。此實現與 HashSet 的不一樣之處在於,LinkedHashSet 維護着一個運行於全部條目的雙重連接列表。此連接列表定義了迭代順序,該迭代順序可爲插入順序或是訪問順序。
看到上面的介紹,是否是感受其與 HashMap 和 LinkedHashMap 的關係很像?
注意,此實現不是同步的。若是多個線程同時訪問連接的哈希Set,而其中至少一個線程修改了該 Set,則它必須保持外部同步。
在LinkedHashMap的實現原理中,經過例子演示了 HashMap 和 LinkedHashMap 的區別。觸類旁通,咱們如今學習的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 的方法。
因此咱們須要注意的點是:
ArrayList 能夠理解爲動態數組,用 MSDN 中的說法,就是 Array 的複雜版本。與 Java 中的數組相比,它的容量能動態增加。ArrayList 是 List 接口的可變數組的實現。實現了全部可選列表操做,並容許包括 null 在內的全部元素。除了實現 List 接口外,此類還提供一些方法來操做內部用來存儲列表的數組的大小。(此類大體上等同於 Vector 類,除了此類是不一樣步的。)
每一個 ArrayList 實例都有一個容量,該容量是指用來存儲列表元素的數組的大小。它老是至少等於列表的大小。隨着向 ArrayList 中不斷添加元素,其容量也自動增加。自動增加會帶來數據向新數組的從新拷貝,所以,若是可預知數據量的多少,可在構造 ArrayList 時指定其容量。在添加大量元素前,應用程序也可使用 ensureCapacity 操做來增長 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 實例的容量。
ArrayList 也採用了快速失敗的機制,經過記錄 modCount 參數來實現。在面對併發的修改時,迭代器很快就會徹底失敗,而不是冒着在未來某個不肯定時間發生任意不肯定行爲的風險。 關於 Fail-Fast 的更詳細的介紹,我在以前將 HashMap 中已經提到。
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 是基於鏈表結構實現,因此在類中包含了 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; } }
該方法是在鏈表的 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++; }
該方法是在指定 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。其本質也就是鏈表之間的刪除添加等。
咱們在以前的博文中瞭解到關於 HashMap 和 Hashtable 這兩種集合。其中 HashMap 是非線程安全的,當咱們只有一個線程在使用 HashMap 的時候,天然不會有問題,但若是涉及到多個線程,而且有讀有寫的過程當中,HashMap 就不能知足咱們的須要了(fail-fast)。在不考慮性能問題的時候,咱們的解決方案有 Hashtable 或者Collections.synchronizedMap(hashMap),這兩種方式基本都是對整個 hash 表結構作鎖定操做的,這樣在鎖表的期間,別的線程就須要等待了,無疑性能不高。
因此咱們在本文中學習一個 util.concurrent 包的重要成員,ConcurrentHashMap。
ConcurrentHashMap 的實現是依賴於 Java 內存模型,因此咱們在瞭解 ConcurrentHashMap 的前提是必須瞭解Java 內存模型。但 Java 內存模型並非本文的重點,因此我假設讀者已經對 Java 內存模型有所瞭解。
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 類中,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 的類定義爲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 的結構中包含的 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 */ @SuppressWarnings("unchecked") 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 變量的讀 / 寫來協調內存可見性,使得 讀操做大多數時候不須要加鎖就能成功獲取到須要的值。因爲散列映射表在實際應用中大多數操做都是成功的 讀操做,因此 2 和 3 既能夠減小請求同一個鎖的頻率,也能夠有效減小持有鎖的時間。經過減少請求同一個鎖的頻率和儘可能減小持有鎖的時間 ,使得 ConcurrentHashMap 的併發性相對於 HashTable 和用同步包裝器包裝的 HashMap有了質的提升。
咱們平時總會有一個電話本記錄全部朋友的電話,可是,若是有朋友常常聯繫,那些朋友的電話號碼不用翻電話本咱們也能記住,可是,若是長時間沒有聯繫了,要再次聯繫那位朋友的時候,咱們又不得不求助電話本,可是,經過電話本查找仍是很費時間的。可是,咱們大腦可以記住的東西是必定的,咱們只能記住本身最熟悉的,而長時間不熟悉的天然就忘記了。
其實,計算機也用到了一樣的一個概念,咱們用緩存來存放之前讀取的數據,而不是直接丟掉,這樣,再次讀取的時候,能夠直接在緩存裏面取,而不用再從新查找一遍,這樣系統的反應能力會有很大提升。可是,當咱們讀取的個數特別大的時候,咱們不可能把全部已經讀取的數據都放在緩存裏,畢竟內存大小是必定的,咱們通常把最近常讀取的放在緩存裏(至關於咱們把最近聯繫的朋友的姓名和電話放在大腦裏同樣)。
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; @Override 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()); } }
HashMap 和 HashSet 都是 collection 框架的一部分,它們讓咱們可以使用對象的集合。collection 框架有本身的接口和實現,主要分爲 Set 接口,List 接口和 Queue 接口。它們有各自的特色,Set 的集合裏不容許對象有重複的值,List 容許有重複,它對集合中的對象進行索引,Queue 的工做原理是 FCFS 算法(First Come, First Serve)。
首先讓咱們來看看什麼是 HashMap 和 HashSet,而後再來比較它們之間的分別。
HashSet 實現了 Set 接口,它不容許集合中有重複的值,當咱們提到 HashSet 時,第一件事情就是在將對象存儲在 HashSet 以前,要先確保對象重寫 equals()和 hashCode()方法,這樣才能比較對象的值是否相等,以確保set中沒有儲存相等的對象。若是咱們沒有重寫這兩個方法,將會使用這個方法的默認實現。
public boolean add(Object o)
方法用來在 Set 中添加元素,當元素值重複時則會當即返回 false,若是成功添加的話會返回 true。
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 中。
HashMap | HashSet |
---|---|
HashMap實現了Map接口 | HashSet實現了Set接口 |
HashMap儲存鍵值對 | HashSet僅僅存儲對象 |
使用put()方法將元素放入map中 | 使用add()方法將元素放入set中 |
HashMap中使用鍵對象來計算hashcode值 | HashSet使用成員對象來計算hashcode值,對於兩個對象來講hashcode可能相同,因此equals()方法用來判斷對象的相等性,若是兩個對象不一樣的話,那麼返回false |
HashMap比較快,由於是使用惟一的鍵來獲取對象 | HashSet較HashMap來講比較慢 |