Map集合,即我們常用的key-Value 集合,Map以鍵值對的形式來存儲數據,我們常用Map集合有:HashMap,TreeMap,WeakHashMap,EnumMap,LinkedHahMap,HashTable。他們都是以key-Value鍵值對形式存儲數據。
1、HashMap
HashMap 底層采用Hash表結構來存儲數據的。
/** * The table, resized as necessary. Length MUST Always be a power of two. */ transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;
我們存儲的數據就存放在Entry 類型的table數組中,Entry 是HashMap一內部類,他實現了接口Map.Entry<K,V> 接口。Entry結構跟我們前面說鏈表Node有點相似。
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; } public final K getKey() { return key; } public final V getValue() { return value; } public final V setValue(V newValue) { V oldValue = value; value = newValue; return oldValue; } public final boolean equals(Object o) { if (!(o instanceof Map.Entry)) return false; Map.Entry e = (Map.Entry)o; Object k1 = getKey(); Object k2 = e.getKey(); if (k1 == k2 || (k1 != null && k1.equals(k2))) { Object v1 = getValue(); Object v2 = e.getValue(); if (v1 == v2 || (v1 != null && v1.equals(v2))) return true; } return false; } public final int hashCode() { return Objects.hashCode(getKey()) ^ Objects.hashCode(getValue()); } public final String toString() { return getKey() + "=" + getValue(); } /** * This method is invoked whenever the value in an entry is * overwritten by an invocation of put(k,v) for a key k that's already * in the HashMap. */ void recordAccess(HashMap<K,V> m) { } /** * This method is invoked whenever the entry is * removed from the table. */ void recordRemoval(HashMap<K,V> m) { } }
當我們使用 new HashMap<K,V>(); 構造方法來創建一HashMap 時,檢測參數是否合法,內部初始化了一些成員變量。
public HashMap() { this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR); } 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); this.loadFactor = loadFactor; threshold = initialCapacity; init(); }
當我們調用put(K k,V v) 方法的時候,底冊又是如何做的呢:
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; }
從上面的代碼中我們可以看出,當我們首次向一個新創建的Map 中put 數據的時候,首先是通過 inflateTable 方法用來初始化用來存數據table數組的長度。初始化長度的同時通過initHashSeedAsNeeded 方法 來初始化哈希掩碼值。
private void inflateTable(int toSize) { // Find a power of 2 >= toSize int capacity = roundUpToPowerOf2(toSize); threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1); table = new Entry[capacity]; initHashSeedAsNeeded(capacity); }
如果我們存放的 key-Value 鍵值對中的鍵值為null, 調用用putForNullKey(value) 方法 來存放我們的鍵值對。
private V putForNullKey(V value) { for (Entry<K,V> e = table[0]; e != null; e = e.next) { if (e.key == null) { V oldValue = e.value; e.value = value; e.recordAccess(this); return oldValue; } } modCount++; addEntry(0, null, value, 0); return null; }
從代碼中我們可以看到,如果key 已存在於原來的map 中,將原來key所對應的value替換為我們新的value,並將原來的value 返回。如過不存在,則調用addEntry方法將我們的key-value 存入 ,並返回null(key)。
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); }
addEntry 方法首先要做的事情是判斷是否需要 擴充table 的長度。如果需要的話調用resize 方法來擴充table 的長度,算出hash ,通過indexFor 方法算出元素所要存放於數組中對應的下標,通過createEntry 方法將元素存入Mpa中。
void createEntry(int hash, K key, V value, int bucketIndex) { Entry<K,V> e = table[bucketIndex]; table[bucketIndex] = new Entry<>(hash, key, value, e); size++; }
如果 存如的 key-value 鍵值對key 不為空。首先通過indexFor 方法算出方法算出元素所要存放於數組中對應的下標i, 從i 開始向后查找元素是否存在於map中如果存在替換原map 中key 所對應的value 值,並將 舊值返回。如果不存在調用addEntry 方法存入。
以上就是map put 值得過程。而當我們通過 value get(Key key) 方法來取值的時候是如何做的呢。
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; }
知道了put 的流程,get 方法理解起來就一目了然了。
下面我們再來看下我們通常用的 containsKey(Key key) 和containsValue(Value value)方法。
public boolean containsKey(Object key) { return getEntry(key) != null; } public boolean containsValue(Object value) { if (value == null) return containsNullValue(); Entry[] tab = table; for (int i = 0; i < tab.length ; i++) for (Entry e = tab[i] ; e != null ; e = e.next) if (value.equals(e.value)) return true; return false; }
是不是有種一目了然地感覺。
綜上所述:我們知道了HashMap 底層數據結構是hash 表結構。我們講到了put 方法、get方法 以及containsValue 和containsKey 方法的內部實現。唯獨沒有說到Hash表到底是中結構,底層的hash 算法是如何實現的。這些會在后面的章節中慢慢說明。
2、TreeMap
TreeMap 底層機構是一種屬結構。TreeMap 具有排序排序功能。 我們在使用TreeMap 的時候可以傳入我們自己的比較器,對其進行排序。如果不傳入的話TreeMap 會使用自己key 默認的比較器進行排序。如果我們想要是用自己定義的類型來作為TreeMap的key 的話 ,我們自定義的類需要實現Comparable 接口實現其compareTo(T o) 方法。或者在使用的時候我們傳入我們自己定義的比較器。下面我們就來看下TreeMap 的底層結構:
跟HashMap 一樣TreeMap 底層也是用 Entry來存儲數據的,但是TreeMap 有自己的Entry 實現(樹)。
static final class Entry<K,V> implements Map.Entry<K,V> { K key; V value; Entry<K,V> left = null; Entry<K,V> right = null; Entry<K,V> parent; boolean color = BLACK; /** * Make a new cell with given key, value, and parent, and with * {@code null} child links, and BLACK color. */ Entry(K key, V value, Entry<K,V> parent) { this.key = key; this.value = value; this.parent = parent; } /** * Returns the key. * * @return the key */ public K getKey() { return key; } /** * Returns the value associated with the key. * * @return the value associated with the key */ public V getValue() { return value; } /** * Replaces the value currently associated with the key with the given * value. * * @return the value associated with the key before this method was * called */ public V setValue(V value) { V oldValue = this.value; this.value = value; return oldValue; } public boolean equals(Object o) { if (!(o instanceof Map.Entry)) return false; Map.Entry<?,?> e = (Map.Entry<?,?>)o; return valEquals(key,e.getKey()) && valEquals(value,e.getValue()); } public int hashCode() { int keyHash = (key==null ? 0 : key.hashCode()); int valueHash = (value==null ? 0 : value.hashCode()); return keyHash ^ valueHash; } public String toString() { return key + "=" + value; } }
知道了TreeMap 底層Entry 的實現我們再來看下 put 方法是如何向TreeMap 中存放數據的。
public V put(K key, V value) { Entry<K,V> t = root; if (t == null) { compare(key, key); // type (and possibly null) check root = new Entry<>(key, value, null); size = 1; modCount++; return null; } int cmp; Entry<K,V> parent; // split comparator and comparable paths Comparator<? super K> cpr = comparator; if (cpr != null) { do { parent = t; cmp = cpr.compare(key, t.key); if (cmp < 0) t = t.left; else if (cmp > 0) t = t.right; else return t.setValue(value); } while (t != null); } else { if (key == null) throw new NullPointerException(); Comparable<? super K> k = (Comparable<? super K>) key; do { parent = t; cmp = k.compareTo(t.key); if (cmp < 0) t = t.left; else if (cmp > 0) t = t.right; else return t.setValue(value); } while (t != null); } Entry<K,V> e = new Entry<>(key, value, parent); if (cmp < 0) parent.left = e; else parent.right = e; fixAfterInsertion(e); size++; modCount++; return null; }
當我們想一TreeMap 中存入第一個元素的時候,底層樹種不存在節點,這是我們創建一個節點做為樹的根節點。節點存放有key 和value 的值(Entry)。以后每次put 分為兩種情況。1、如果在我門 new TreeMap 的時候傳入了我們自己的比較器 ,就采用我們自己定義的比較器來比較key 值得大小,來確定元素在書中存放的位置。2、如果沒有傳入我們自己定義的比較器(這中情況下 我們的key 類型需要實現Comparable 接口)使用key 默認實現的 比較方法。
在來看以下 TreeMap 的get 方法:
public V get(Object key) { Entry<K,V> p = getEntry(key); return (p==null ? null : p.value); } final Entry<K,V> getEntry(Object key) { // Offload comparator-based version for sake of performance if (comparator != null) return getEntryUsingComparator(key); if (key == null) throw new NullPointerException(); Comparable<? super K> k = (Comparable<? super K>) key; Entry<K,V> p = root; while (p != null) { int cmp = k.compareTo(p.key); if (cmp < 0) p = p.left; else if (cmp > 0) p = p.right; else return p; } return null; }
上面我們看到了 get 方法的實現。無非就是 通過我們自己傳入的比較器,或者key 自身的比較方法來來尋找相同的key 值。找到的話返回當前key 所對應的 value 找不到的話返回null。
同樣containsKey 和 containsValue 實現思路差不多。
3、WeakHashMap
WeakHashMap實現了Map接口,是HashMap的一種實現,他使用弱引用作為內部數據的存儲方案,WeakHashMap可以作為簡單緩存表的解決方案,當系統內存不夠的時候,垃圾收集器會自動的清除沒有在其他任何地方被引用的鍵值對。
如果需要用一張很大的HashMap作為緩存表,那么可以考慮使用WeakHashMap,當鍵值不存在的時候添加到表中,存在即取出其值。
WeakHashMap weakMap = new WeakHashMap<Integer, byte[]>(); for(int i = 0; i < 100000; i++){ Integer ii = new Integer(i); weakMap.put(ii, new byte[i]); } HashMap map = new HashMap<Integer, byte[]>(); for (int i = 0; i < 10000; i++) { Integer ii = new Integer(i); map.put(ii, new byte[i]); }
這2段代碼分別用-Xmx2M的參數運行,運行的結果是第一段代碼可以很好的運行,第二段代碼會出現“Java Heap Space”的錯誤,這說明用WeakHashMap存儲,在系統內存不夠用的時候會自動回收內存。
如果WeakHashMap的key在系統內持有強引用,那么WeakHashMap就退化為HashMap,所有的表項無法被垃圾收集器自動清理。
4、EnumMap
與枚舉類型鍵一起使用的專用 Map
實現。枚舉映射中所有鍵都必須來自單個枚舉類型,該枚舉類型在創建映射時顯式或隱式地指定。枚舉映射在內部表示為數組。此表示形式非常緊湊且高效。
枚舉映射根據其鍵的自然順序 來維護(該順序是聲明枚舉常量的順序)。在 collection 視圖(keySet()
、entrySet()
和 values()
)所返回的迭代器中反映了這一點。
由 collection 視圖返回的迭代器是弱一致 的:它們不會拋出 ConcurrentModificationException
,也不一定顯示在迭代進行時發生的任何映射修改的效果。
不允許使用 null 鍵。試圖插入 null 鍵將拋出 NullPointerException
。但是,試圖測試是否出現 null 鍵或移除 null 鍵將不會拋出異常。允許使用 null 值。
像大多數 collection 一樣,EnumMap 是不同步的。如果多個線程同時訪問一個枚舉映射,並且至少有一個線程修改該映射,則此枚舉映射在外部應該是同步的。這一般通過對自然封裝該枚舉映射的某個對象進行同步來完成。如果不存在這樣的對象,則應該使用 Collections.synchronizedMap(java.util.Map<k, v="">)
方法來“包裝”該枚舉。最好在創建時完成這一操作,以防止意外的非同步訪問:
Map<EnumKey, V> m = Collections.synchronizedMap(new EnumMap<EnumKey, V>(...));
實現注意事項:所有基本操作都在固定時間內執行。雖然並不保證,但它們很可能比其 HashMap
副本更快。
下面我們簡單來看下 EnumMap 底層簡單實現:
先從構造方法:
private final Class<K> keyType; private transient K[] keyUniverse; private transient Object[] vals; public EnumMap(Class<K> keyType) { this.keyType = keyType; keyUniverse = getKeyUniverse(keyType); vals = new Object[keyUniverse.length]; } public EnumMap(EnumMap<K, ? extends V> m) { keyType = m.keyType; keyUniverse = m.keyUniverse; vals = m.vals.clone(); size = m.size; } public EnumMap(Map<K, ? extends V> m) { if (m instanceof EnumMap) { EnumMap<K, ? extends V> em = (EnumMap<K, ? extends V>) m; keyType = em.keyType; keyUniverse = em.keyUniverse; vals = em.vals.clone(); size = em.size; } else { if (m.isEmpty()) throw new IllegalArgumentException("Specified map is empty"); keyType = m.keySet().iterator().next().getDeclaringClass(); keyUniverse = getKeyUniverse(keyType); vals = new Object[keyUniverse.length]; putAll(m); } }
EnumMap 提供了三種不同參數的構造方法。
再來看下put 方法:
public V put(K key, V value) { typeCheck(key); int index = key.ordinal(); Object oldValue = vals[index]; vals[index] = maskNull(value); if (oldValue == null) size++; return unmaskNull(oldValue); } private Object maskNull(Object value) { return (value == null ? NULL : value); } private V unmaskNull(Object value) { return (V) (value == NULL ? null : value); }
從中我們可以看出put 方法插入值得時候 是想vals 數組中加入了數據。
再看get 方法:
public V get(Object key) { return (isValidKey(key) ? unmaskNull(vals[((Enum)key).ordinal()]) : null); } private boolean isValidKey(Object key) { if (key == null) return false; // Cheaper than instanceof Enum followed by getDeclaringClass Class keyClass = key.getClass(); return keyClass == keyType || keyClass.getSuperclass() == keyType; }
從上面我們可以看出:枚舉映射在內部表示為數組。
5、LinkedHashMap
LinkedHashMap是HashMap的一個子類,它保留插入的順序,如果需要輸出的順序和輸入時的相同,那么就選用LinkedHashMap。
LinkedHashMap是Map接口的哈希表和鏈接列表實現,具有可預知的迭代順序。此實現提供所有可選的映射操作,並允許使用null值和null鍵。此類不保證映射的順序,特別是它不保證該順序恆久不變。
LinkedHashMap實現與HashMap的不同之處在於,后者維護着一個運行於所有條目的雙重鏈接列表。此鏈接列表定義了迭代順序,該迭代順序可以是插入順序或者是訪問順序。
對於LinkedHashMap而言,它繼承與HashMap、底層使用哈希表與雙向鏈表來保存所有元素。其基本操作與父類HashMap相似,它通過重寫父類相關的方法,來實現自己的鏈接列表特性。下面我們來分析LinkedHashMap的源代碼:
public class LinkedHashMap<K, V> extends HashMap<K, V> implements Map<K, V>
LinkedHashMap采用的hash算法和HashMap相同,但是它重新定義了數組中保存的元素Entry,該Entry除了保存當前對象的引用外,還保存了其上一個元素before和下一個元素after的引用,從而在哈希表的基礎上又構成了雙向鏈接列表。看源代碼:
//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; …… }
HashMap.Entry:
static class Entry<K,V> implements Map.Entry<K,V> { final K key; V value; Entry<K,V> next; final int hash; Entry(int h, K k, V v, Entry<K,V> n) { value = v; next = n; key = k; hash = h; } }
通過源代碼可以看出,在LinkedHashMap的構造方法中,實際調用了父類HashMap的相關構造方法來構造一個底層存放的table數組。如:
public LinkedHashMap(int initialCapacity, float loadFactor) { super(initialCapacity, loadFactor); accessOrder = false; }
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)(capacity * loadFactor); table = new Entry[capacity]; init(); }
我們已經知道LinkedHashMap的Entry元素繼承HashMap的Entry,提供了雙向鏈表的功能。在上述HashMap的構造器中,最后會調用init()方法,進行相關的初始化,這個方法在HashMap的實現中並無意義,只是提供給子類實現相關的初始化調用。
LinkedHashMap重寫了init()方法,在調用父類的構造方法完成構造后,進一步實現了對其元素Entry的初始化操作。
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 方法:
public V put(K key, V value) { if (key == null) return putForNullKey(value); int hash = hash(key.hashCode()); 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; }
6、HashTable 和HashMap 的區別:
HashTable 和HashMap 大致相同,我們這部在累述,在這只說下他們的區別:
public class Hashtable<K,V> extends Dictionary<K,V> implements Map<K,V>, Cloneable, java.io.Serializable public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable
Hashtable 繼承自Dictionary HashMap 繼承自AbstractMap。
HashTable 的put 方法:
public synchronized V put(K key, V value) { // Make sure the value is not null if (value == null) { throw new NullPointerException(); } // Makes sure the key is not already in the hashtable. 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(); tab = table; hash = hash(key); index = (hash & 0x7FFFFFFF) % tab.length; } // Creates the new entry. Entry<K,V> e = tab[index]; tab[index] = new Entry<>(hash, key, value, e); count++; return null; }
注意1 方法是同步的
注意2 方法不允許value==null
注意3 方法調用了key的hashCode方法,如果key==null,會拋出空指針異常 HashMap的put方法如下
HashMap 的put
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; }
注意1 方法是非同步的
注意2 方法允許key==null
注意3 方法並沒有對value進行任何調用,所以允許為null