Map 就是另一個頂級介面了,總感覺 Map 是 Collection 的子介面呢。Map 主要用於表示那些含有映射關係的資料,儲存的是一組一組的索引值對。Map 是允許你將某些對象與其它一些對象關聯起來的關聯陣列。
舉個例子感受一下:我想通過學生的學號來找到對應的姓名就可以使用 Map 來儲存 Map< Integer ,String > 。我想知道每個學生一共選了幾門課可以這樣儲存 Map < Student ,List < Course > > 。這樣我們就將 Student 這個類和課程的集合 List < Course > 關聯起來了 。
下面來說說 Map 這個頂級的介面都有哪些具體的實現。
HashMap :它根據鍵的 hashCode 值儲存資料,大多數情況下可以直接定位到它的值,因而具有很快的訪問速度,但遍曆順序卻是不確定的。 HashMap 最多隻允許一條記錄的鍵為 null ,允許多條記錄的值為 null 。HashMap 非安全執行緒,即任一時刻可以有多個線程同時寫 HashMap ,可能會導致資料的不一致。如果需要滿足安全執行緒,可以用 Collections 的 synchronizedMap 方法使HashMap 具有安全執行緒的能力,或者使用 ConcurrentHashMap 。
Hashtable :Hashtable 是遺留類,很多映射的常用功能與 HashMap 類似,不同的是它承自 Dictionary 類,並且是安全執行緒的,任一時間只有一個線程能寫 Hashtable,並發性不如 ConcurrentHashMap,因為 ConcurrentHashMap 引入了分段鎖。Hashtable 不建議在新代碼中使用,不需要安全執行緒的場合可以用 HashMap 替換,需要安全執行緒的場合可以用 ConcurrentHashMap 替換。
LinkedHashMap :LinkedHashMap 是 HashMap 的一個子類,儲存了記錄的插入順序,在用 Iterator 遍曆 LinkedHashMap 時,先得到的記錄肯定是先插入的,也可以在構造時帶參數,按照訪問次序排序 。
TreeMap :TreeMap 實現 SortedMap 介面,能夠把它儲存的記錄根據鍵排序,預設是按索引值的升序排序,也可以指定排序的比較子,當用 Iterator 遍曆 TreeMap 時,得到的記錄是排過序的。如果使用排序的映射,建議使用 TreeMap 。在使用 TreeMap 時,key 必須實現 Comparable 介面或者在構造 TreeMap 時傳入自訂的 Comparator ,否則會在運行時拋出ClassCastException 類型的異常。
對於上述四種 Map 類型的類,要求映射中的 key 是不可變對象。不可變對象是該對象在建立後它的雜湊值不會被改變 。可以參考這篇文章理解,String 與不可變對象。如果對象的雜湊值發生變化,Map 對象很可能就定位不到映射的位置了
HashMap 的底層主要是基於 hash 表,首先來介紹一下 hash 相關的知識。hash 又名散列,hash 表也就是散列表,hash 表的出現是為了使得資料的尋找變得簡單,快速,原理是根據關鍵字的 hashCode 值來確定該關鍵字儲存的位置。而計算出 hashCode 值的方法也就是 hash 演算法,若是不同的關鍵字計算出同一個 hashCode 值,那麼就會儲存在同一個位置上,此時也就發生了衝突。
我們想要在空間有限的前提下,盡量減少衝突的發生,從而保證我們的尋找效率不受影響,就需要設計一個好的 hash 演算法,也要充分考慮到當發生衝突了應該怎麼辦。
那就來看看 HashMap 中是怎麼來設計的,主要體現在 hash 演算法的設計,使用鏈表結構和 resize 。首先看一下有哪些重要的屬性
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable{ // 預設長度 static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16 // 最大容量 static final int MAXIMUM_CAPACITY = 1 << 30; // 預設負載因子 static final float DEFAULT_LOAD_FACTOR = 0.75f; // 初始化為空白 static final Entry<?,?>[] EMPTY_TABLE = {}; // 存放索引值對的 entry 數組 transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE; // 實際長度 transient int size; // rehash 之前的最大長度 等於 DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY int threshold; final float loadFactor; /** * The number of times this HashMap has been structurally modified * the HashMap fail-fast. (See ConcurrentModificationException). */ transient int modCount; static class Entry<K,V> implements Map.Entry<K,V> { final K key; V value; Entry<K,V> next; int hash; /** * Creates new entry. */ Entry(int h, K k, V v, Entry<K,V> n) { value = v; next = n; key = k; hash = h; }
HashMap 用一個數組來表示 Hash 表,而數組中的每一個元素就是一個entry,也就是一個索引值對。當發生衝突的時候,我們可以在同一個位置中存放多個 entry ,此時的結構是鏈表,通過 entry 中的 next 指向下一個 entry 。以上源碼均來自 JDK 1.7 ,在 JDK 1.8 中 entry 變成了 Node 節點,而且若是當同一位置中的元素數量大於 8 這個閥值的時候,鏈表結構會變成紅/黑樹狀結構,這樣做的原因是可以大大加快元素的尋找速度。
說完了 HashMap 中的結構,我們再來看看具體的操作。主要是 entry 的 put 和 get 過程,put 的過程,我放一張圖,過程可一目瞭然。
map.put("name" , "YJK923"); 這個過程就是通過 hashCode 方法計算 name 這個 String 對象的 hashCode 值(說一句,hashCode 這個方法是 Object 對象的,且是一個 native 方法)得到這個 hashCode 值之後還不能直接進行映射數組下標儲存資料,為了使資料盡量不散落在同一位置,還多了一步 hash 值和index 值轉化的步驟。
public V put(K key, V value) { if (table == EMPTY_TABLE) { inflateTable(threshold); } 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; } static int indexFor(int h, int length) { return h & (length-1); }
看完了 put 看 get ,get 方法通過傳入的 key 值計算出 hash 值再得出索引值,若是同一位置有多個元素,則再使用 key 的 equals 方法找到指定的 entry 。最終取出相應的 entry ,但是返回我我們的就是一個 value 值而已。
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) { if (size == 0) { return null; } 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; }
最後說一下 rehash 的過程,為什麼會出現 rehash ,是因為實際長度已經達到了 threshold ,也就是 loadFactor * capacity 。設定這個值的原因就是為了防止過多的元素落在同一個桶中,增加了衝突的發生,及時的增加長度。我們知道 HashMap 的預設長度是 16 ,而若是發生了 rehash ,長度直接翻倍。且 resize 的過程中會重新建立一個新的 entry 數組來存放原有的資料,且所有的 entry 都會重新計算 hash 值。resize 也就是擴容,在擴容的時候會 rehash 。
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 resize(int newCapacity) { Entry[] oldTable = table; int oldCapacity = oldTable.length; if (oldCapacity == MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return; } Entry[] newTable = new Entry[newCapacity]; transfer(newTable, initHashSeedAsNeeded(newCapacity)); table = newTable; threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1); } void transfer(Entry[] newTable, boolean rehash) { int newCapacity = newTable.length; for (Entry<K,V> e : table) { while(null != e) { Entry<K,V> next = e.next; if (rehash) { e.hash = null == e.key ? 0 : hash(e.key); } int i = indexFor(e.hash, newCapacity); e.next = newTable[i]; newTable[i] = e; e = next; } } }
通過改進 hash 演算法,增加鏈表的儲存結構,rehash 等操作,我們就可以將原始的 hash 表和 hash 演算法應用到 JDK 1.7 中的 HashMap 中,然而在 JDK 1.8 中又對 HashMap 的功能進行了增強,主要體現在以下方面 1 當鏈表的長度大於 8 的時候,就將鏈錶轉化為紅/黑樹狀結構 。2 改進了 hash 的過程,也就是 key 映射到 index 這個過程進行增強,降低了衝突發生的可能。 3 對 rehash 的增強,使其不用重新計算之前 entry 的 index 值。
最後還補充一點關於 Map 的遍曆,有幾下幾種方式:
1 擷取 key 的集合 keySet 。
2 擷取 value 的集合 Collection 。
3 擷取 entry 的集合 entrySet 。
Set<Integer> keySet = map.keySet();Iterator<Integer> it2 = keySet.iterator();while(it2.hasNext()){ System.out.println(it2.next());}Collection<String> values = map.values();Iterator<String> it = values.iterator();while(it.hasNext()){ System.out.println(it.next());}Set<Entry<Integer,String>> entrySet = map.entrySet();Iterator<Entry<Integer, String>> iterator = entrySet.iterator();while (iterator.hasNext()) { Entry<Integer, String> entry = iterator.next(); System.out.println(entry.getKey() + " ----> "+ entry.getValue());}
最後我想問,還有誰 ?還在使用 JDK1.7 。
我 ~
推薦閱讀: Java 集合之 Collection
參考資料:Java8系列之重新認識HashMap