一些最經常使用的Map實現類是HashMap,LinkedHashMap,TreeMap,SortedMap,HashTable,WeakedHashMap。html
Set的實現類都是基於Map來實現的(如,HashSet是經過HashMap實現的,TreeSet是經過TreeMap實現的,LinkedHashSet是經過LinkedHashMap來實現的)。 java
void clear() //今後映射中移除全部映射關係(可選操做)。 boolean containsKey(Object key) //若是此映射包含指定鍵的映射關係,則返回 true。 boolean containsValue(Object value) //若是此映射將一個或多個鍵映射到指定值,則返回 true。 Set<Map.Entry<K,V>> entrySet() //返回此映射中包含的映射關係的 Set 視圖。 boolean equals(Object o) //比較指定的對象與此映射是否相等。 V get(Object key) //返回指定鍵所映射的值;若是此映射不包含該鍵的映射關係,則返回 null。 int hashCode() //返回此映射的哈希碼值。 boolean isEmpty() //若是此映射未包含鍵-值映射關係,則返回 true。 Set<K> keySet() //返回此映射中包含的鍵的 Set 視圖。 V put(K key, V value) //將指定的值與此映射中的指定鍵關聯(可選操做)。 void putAll(Map<? extends K,? extends V> m) //從指定映射中將全部映射關係複製到此映射中(可選操做)。 V remove(Object key) //若是存在一個鍵的映射關係,則將其今後映射中移除(可選操做)。 int size() //返回此映射中的鍵-值映射關係數。 Collection<V> values() //返回此映射中包含的值的 Collection 視圖。
在JDK1.6,JDK1.7中,HashMap採用位桶+鏈表實現,即便用鏈表處理衝突,同一hash值的鏈表都存儲在一個鏈表裏。可是當位於一個桶中的元素較多,即hash值相等的元素較多時,經過key值依次查找的效率較低。而JDK1.8中,HashMap採用位桶+鏈表+紅黑樹實現,當鏈表長度超過閾值(8)時,將鏈表轉換爲紅黑樹,這樣大大減小了查找時間。node
HashMap 繼承了AbstractMap,實現了Map<K,V>、Cloneable和Serializable接口!面試
HashMap在觸發擴容後,閾值會變爲原來的2倍,而且會進行重hash,重hash後索引位置index的節點的新分佈位置最多隻有兩個:原索引位置或原索引+oldCap位置。例如capacity爲16,索引位置5的節點擴容後,只可能分佈在新報索引位置5和索引位置21(5+16)。致使HashMap擴容後,同一個索引位置的節點重hash最多分佈在兩個位置的根本緣由是:1)table的長度始終爲2的n次方;2)索引位置的計算方法爲「(table.length - 1) & hash」。HashMap擴容是一個比較耗時的操做,定義HashMap時儘可能給個接近的初始容量值。【首次put元素須要進行擴容爲默認容量16,閾值16*0.75=12,之後擴容後的table大小變爲原來的兩倍,接下來就是進行擴容後table的調整:假設擴容前的table大小爲2的N次方,有上述put方法解析可知,元素的table索引爲其hash值的後N位肯定那麼擴容後的table大小即爲2的N+1次方,則其中元素的table索引爲其hash值的後N+1位肯定,比原來多了一位所以,table中的元素只有兩種狀況:元素hash值第N+1位爲0:不須要進行位置調整;元素hash值第N+1位爲1:調整至原索引的兩倍位置;擴容或初始化完成後,resize方法返回新的table。】算法
put(key,value)的過程:1. 當桶數組 table 爲空或者null時,不然以默認大小resize();2.根據鍵值key計算hash值獲得插入的數組索引i,若是tab[i]==null,直接新建節點添加,不然判斷當前數組中處理hash衝突的方式爲鏈表仍是紅黑樹(check第一個節點類型便可),分別處理;3. 查找要插入的鍵值對已經存在,存在的話根據條件判斷是否用新值替換舊值;4.若是不存在,則將鍵值對鏈入鏈表中,並根據鏈表長度決定是否將鏈表轉爲紅黑樹;5.判斷鍵值對數量是否大於閾值,大於的話則進行擴容操做編程
HasMap的擴容機制resize():構造hash表時,若是不指明初始大小,默認大小爲16(即Node數組大小16),若是Node[]數組中的元素達到(填充比*Node.length)從新調整HashMap大小 變爲原來2倍大小,擴容很耗時segmentfault
HashMap有threshold屬性和loadFactor屬性,可是沒有capacity屬性。初始化時,若是傳了初始化容量值,該值是存在threshold變量,而且Node數組是在第一次put時纔會進行初始化,初始化時會將此時的threshold值做爲新表的capacity值,而後用capacity和loadFactor計算新表的真正threshold值。api
重寫計算hash是經過key的hashCode的高16位和低16位異或後和桶的數量取模獲得索引位置,即key.hashcode()^(hashcode>>>16)%length,;好處:1.讓高位數據與低位數據進行異或,以此加大低位信息的隨機性,變相的讓高位數據參與到計算中。2. 能夠增長 hash 的複雜度,進而影響 hash 的分佈性。這也就是爲何 HashMap 不直接使用鍵對象原始 hash 的緣由了。【在 Java 中,hashCode 方法產生的 hash 是 int 類型,32 位寬。前16位爲高位,後16位爲低位,因此要右移16位。】數組
當同一個索引位置的節點在增長後達到9個時,而且此時數組的長度大於等於64,則會觸發鏈表節點(Node)轉紅黑樹節點(TreeNode,間接繼承Node),轉成紅黑樹節點後,其實鏈表的結構還存在,經過next屬性維持。鏈表節點轉紅黑樹節點的具體方法爲源碼中的treeifyBin(Node<K,V>[] tab, int hash)方法。而若是數組長度小於64,則不會觸發鏈表轉紅黑樹,而是會進行擴容。緩存
當同一個索引位置的節點在移除後達到6個時,而且該索引位置的節點爲紅黑樹節點,會觸發紅黑樹節點轉鏈表節點。紅黑樹節點轉鏈表節點的具體方法爲源碼中的untreeify(HashMap<K,V> map)方法。
保證鍵的惟一性,須要覆蓋hashCode方法,和equals方法。先寫hashCode再寫equals 一、若是兩個對象相同(即用equals比較返回true),那麼它們的hashCode值必定要相同;二、若是兩個對象的hashCode相同,它們並不必定相同(即用equals比較返回false) 【由於equals()方法只比較兩個對象是否相同,至關於==,而不一樣的對象hashCode()確定是不一樣,因此若是咱們不是看對象,而只看對象的屬性,則要重寫這兩個方法,如Integer和String他們的equals()方法都是重寫過了,都只是比較對象裏的內容。使用HashMap,若是key是自定義的類,默認的equal函數的行爲可能不能符合咱們的要求,就必須重寫hashcode()和equals()。】
序列化:桶數組 table 被申明爲 transient。HashMap 並無使用默認的序列化機制,而是經過實現readObject/writeObject
兩個方法自定義了序列化的內容。【序列化 talbe 存在着兩個問題:1.transient 是代表該數據不參與序列化。由於 HashMap 中的存儲數據的數組數據成員中,數組還有不少的空間沒有被使用,沒有被使用到的空間被序列化沒有意義,浪費空間。因此須要手動使用 writeObject() 方法,只序列化實際存儲元素的數組。;2.同一個鍵值對在不一樣 JVM 下,所處的桶位置多是不一樣的,在不一樣的 JVM 下反序列化 table 可能會發生錯誤。(HashMap 的get/put/remove
等方法第一步就是根據 hash 找到鍵所在的桶位置,但若是鍵沒有覆寫 hashCode 方法,計算 hash 時最終調用 Object 中的 hashCode 方法。但 Object 中的 hashCode 方法是 native 型的,不一樣的 JVM 下,可能會有不一樣的實現,產生的 hash 可能也是不同的。也就是說同一個鍵在不一樣平臺下可能會產生不一樣的 hash,此時再對在同一個 table 繼續操做,就會出現問題。)】
基於紅黑樹(Red-Black tree)的 NavigableMap
實現。該映射根據其鍵的天然順序進行排序,或者根據建立映射時提供的 Comparator
進行排序,具體取決於使用的構造方法。
此實現爲 containsKey、get、put 和 remove 操做提供受保證的 log(n) 時間開銷。
TreeMap會自動排序,若是存放的對象不能排序則會報錯,因此存放的對象必須指定排序規則。排序規則包括天然排序和客戶排序。
①天然排序:TreeMap要添加哪一個對象就在哪一個對象類上面實現java.lang.Comparable接口,而且重寫comparaTo()方法,返回0則表示是同一個對象,不然爲不一樣對象。
②客戶排序:創建一個第三方類並實現java.util.Comparator接口。並重寫方法。定義集合形式爲TreeMap tm = new TreeMap(new 第三方類());
TreeMap繼承了AbstractMap,實現了NavigableMap、Cloneable和Serializable接口!
LinkedHashMap類:LinkedHashMap正好介於HashMap和TreeMap之間,它也是一個hash表,但它同時維護了一個雙鏈表來記錄插入的順序,基本方法的複雜度爲O(1)。
當遍歷該集合時候,LinkedHashMap將會以元素的添加順序訪問集合的元素。
HashMap
(及 Hashtable
)所提供的一般爲雜亂無章的排序工做,同時無需增長與 TreeMap
相關的成本。使用它能夠生成一個與原來順序相同的映射副本,而與原映射的實現無關。【Map copy = new LinkedHashMap(m);】LinkedHashMap是如何實現LRU的。首先,當accessOrder爲true時,纔會開啓按訪問順序排序的模式,才能用來實現LRU算法。咱們能夠看到,不管是put方法仍是get方法,都會致使目標Entry成爲最近訪問的Entry,所以便把該Entry加入到了雙向鏈表的末尾(get方法經過調用recordAccess方法來實現,put方法在覆蓋已有key的狀況下,也是經過調用recordAccess方法來實現,在插入新的Entry時,則是經過createEntry中的addBefore方法來實現),這樣便把最近使用了的Entry放入到了雙向鏈表的後面,屢次操做後,雙向鏈表前面的Entry即是最近沒有使用的,這樣當節點個數滿的時候,刪除的最前面的Entry(head後面的那個Entry)即是最近最少使用的Entry。
此類實現一個哈希表,該哈希表將鍵映射到相應的值。任何非 null
對象均可以用做鍵或值。
爲了成功地在哈希表中存儲和獲取對象,用做鍵的對象必須實現 hashCode
方法和 equals
方法。
null
對象均可以用做鍵或值。初始時已經構建了數據結構是Entry類型的數組,Entry源碼和hashmap基本元素用的node基本是同樣的hashCode
方法和 equals
方法。rehash
操做所須要的時間損耗之間的平衡。若是初始容量大於 Hashtable 所包含的最大條目數除以加載因子,則永遠 不會發生 rehash
操做。可是,將初始容量設置過高可能會浪費空間。若是不少條目要存儲在一個 Hashtable
中,那麼與根據須要執行自動 rehashing 操做來增大表的容量的作法相比,使用足夠大的初始容量建立哈希表或許能夠更有效地插入條目。Hashtable中key和value都不容許爲null,而HashMap中key和value都容許爲null(key只能有一個爲null,而value則能夠有多個爲null)。可是若是在Hashtable中有相似put(null,null)的操做,編譯一樣能夠經過,由於key和value都是Object類型,但運行時會拋出NullPointerException異常,這是JDK的規範規定的。
Hashtable計算hash值,直接用key的hashCode(),而HashMap從新計算了key的hash值,Hashtable在求hash值對應的位置索引時,用取模運算,而HashMap在求位置索引時,則用與運算,且這裏通常先用hash&0x7FFFFFFF後,再對length取模,&0x7FFFFFFF的目的是爲了將負的hash值轉化爲正值,由於hash值有可能爲負數,而&0x7FFFFFFF後,只有符號外改變,然後面的位都不變。
以弱鍵 實現的基於哈希表的 Map。在 WeakHashMap 中,當某個鍵再也不正常使用時,將自動移除其條目。更精確地說,對於一個給定的鍵,其映射的存在並不阻止垃圾回收器對該鍵的丟棄,這就使該鍵成爲可終止的,被終止,而後被回收。丟棄某個鍵時,其條目從映射中有效地移除,所以,該類的行爲與其餘的 Map 實現有所不一樣。
getTable
的方法,而getTable
裏又調用了expungeStaleEntries,
清空table中無用鍵值對。原理以下:新建WeakHashMap,將「鍵值對」添加到WeakHashMap中。當WeakHashMap中某個「弱引用的key」因爲沒有再被引用而被GC收回時,在GC回收該「弱鍵」時,這個「弱鍵」也同時會被添加到"ReferenceQueue(queue)"中。 當下一次咱們須要操做WeakHashMap時,會先同步table和queue。table中保存了所有的鍵值對,而queue中保存被GC回收的鍵值對;同步它們,就是刪除table中被GC回收的鍵值對。當咱們執行expungeStaleEntries時,就遍歷"ReferenceQueue(queue)"中的全部key,而後就在「WeakReference的table」中刪除與「ReferenceQueue(queue)中key」對應的鍵值對。tomcat在ConcurrentCache是使用ConcurrentHashMap和WeakHashMap作了分代的緩存。在put方法裏,在插入一個k-v時,先檢查eden緩存的容量是否是超了。沒有超就直接放入eden緩存,若是超了則鎖定longterm將eden中全部的k-v都放入longterm。再將eden清空並插入k-v。在get方法中,也是優先從eden中找對應的v,若是沒有則進入longterm緩存中查找,找到後就加入eden緩存並返回。 通過這樣的設計,相對經常使用的對象都能在eden緩存中找到,不經常使用(有可能被銷燬的對象)的則進入longterm緩存。而longterm的key的實際對象沒有其餘引用指向它時,gc就會自動回收heap中該弱引用指向的實際對象,弱引用進入引用隊列。longterm調用expungeStaleEntries()方法,遍歷引用隊列中的弱引用,並清除對應的Entry,不會形成內存空間的浪費。
遍歷Map,並獲取其 <Key, Value> 的方法有兩種:
(1)KeySet<KeyType>
(2)EntrySet<KeyType, VlaueType>(性能更好)
EntrySet速度比KeySet快了兩倍多點;
差異在哪裏呢? 源碼給咱們答案了。
public V get(Object key) { if (key == null) return getForNullKey(); Entry<K,V> entry = getEntry(key); return null == entry ? null : entry.getValue(); }
(1)在須要同時獲取Map的<Key, Value>時,EntrySet<KeyType, VlaueType>比KeySet<KeyType>方法要快不少。
(2)若是隻須要獲取Map的Key,建議使用KeySet<KeyType>方法,由於不須要像EntrySet<KeyType, VlaueType>同樣開闢額外的空間存儲value值。
(3)若是隻須要獲取Map的Value,建議使用map.values()方法獲取values的集合(Collection)。
(4)因爲操做系統內存管理的置換算法(LRU,Least Recently Used,近期最少使用算法),屢次遍歷速度會逐漸增長(直到寄存器被佔滿),由於經常使用數據會從主存被緩存到寄存器中。
keySet()方法返回一個引用,這個引用指向了HashMap的一個內部類KeySet類,此內部類繼承了AbstractSet,此內部類初始化迭代器產生一個迭代器對象KeyIterator,它繼承了HashIterator迭代器,HashIterator迭代器初始化拿到了next指向map中的第一個元素。當使用keySet集合遍歷key時,實際上是使用迭代器KeyIterator迭代每一個節點的key。
entrySet()方法同理。
1 import java.util.Collection; 2 import java.util.HashMap; 3 import java.util.Iterator; 4 import java.util.Map; 5 import java.util.Map.Entry; 6 import java.util.Set; 7 8 public class MapDemo { 9 10 public static Map<Integer, String> map; 11 static { 12 map = new HashMap<Integer, String>(); 13 for(int i=0;i<1000000;i++) { 14 map.put(3*i+1, "China"); 15 map.put(3*i+2, "America"); 16 map.put(3*i+3, "Japan"); 17 } 18 } 19 20 public static void main(String[] args) { 21 System.out.println("map keySet ellipse " + MapKeySetMethod() + " ms"); 22 System.out.println("map entrySet ellipse " + MapEntrySetMethod() + " ms"); 23 //爲了排除所謂的緩存帶來的干擾,這裏再多執行幾回 24 System.out.println("map keySet ellipse " + MapKeySetMethod() + " ms"); 25 System.out.println("map entrySet ellipse " + MapEntrySetMethod() + " ms"); 26 System.out.println("map keySet ellipse " + MapKeySetMethod() + " ms"); 27 System.out.println("map entrySet ellipse " + MapEntrySetMethod() + " ms"); 28 System.out.println("map keySet ellipse " + MapKeySetMethod() + " ms"); 29 System.out.println("map entrySet ellipse " + MapEntrySetMethod() + " ms"); 30 31 //當只要獲取其value的時候能夠這麼用 32 Collection<String> values = map.values(); 33 for(String str : values) { 34 //System.out.println(str); 35 } 36 //當只要獲取其key的時候能夠這麼用 37 Collection<Integer> keys = map.keySet(); 38 for(Integer key : keys) { 39 //System.out.println(key); 40 } 41 } 42 43 public static long MapKeySetMethod() { 44 long startTime = System.currentTimeMillis(); 45 Set<Integer> keySet = map.keySet(); 46 Iterator<Integer> iterator = keySet.iterator(); 47 while(iterator.hasNext()) { 48 Integer key = iterator.next(); 49 String value = map.get(key); 50 //System.out.println(key + " = " + value); 51 } 52 long endTime = System.currentTimeMillis(); 53 return endTime-startTime; 54 } 55 56 public static long MapEntrySetMethod() { 57 long startTime = System.currentTimeMillis(); 58 Set<Entry<Integer, String>> entrySet = map.entrySet(); 59 Iterator<Entry<Integer, String>> iterator = entrySet.iterator(); 60 while(iterator.hasNext()) { 61 Entry<Integer, String> entry = iterator.next(); 62 Integer key = entry.getKey(); 63 String value = entry.getValue(); 64 //System.out.println(key + " = " + value); 65 } 66 long endTime = System.currentTimeMillis(); 67 return endTime-startTime; 68 } 69 }
C
可縮放的併發 ConcurrentNavigableMap
實現。映射能夠根據鍵的天然順序進行排序,也能夠根據建立映射時所提供的 Comparator
進行排序,具體取決於使用的構造方法。
此類實現 SkipLists 的併發變體,爲 containsKey、get、put、remove 操做及其變體提供預期平均 log(n) 時間開銷。多個線程能夠安全地併發執行插入、移除、更新和訪問操做。迭代器是弱一致 的,返回的元素將反映迭代器建立時或建立後某一時刻的映射狀態。它們不 拋出 ConcurrentModificationException
,能夠併發處理其餘操做。升序鍵排序視圖及其迭代器比降序鍵排序視圖及其迭代器更快。
此類及此類視圖中的方法返回的全部 Map.Entry 對,表示他們產生時的映射關係快照。它們不 支持 Entry.setValue 方法。(注意,根據所需效果,可使用 put、putIfAbsent 或 replace 更改關聯映射中的映射關係。)
請注意,與在大多數 collection 中不一樣,這裏的 size 方法不是 一個固定時間 (constant-time) 操做。由於這些映射的異步特性,肯定元素的當前數目須要遍歷元素。此外,批量操做 putAll、equals 和 clear 並不 保證能以原子方式 (atomically) 執行。例如,與 putAll 操做一塊兒併發操做的迭代器只能查看某些附加元素。
支持獲取的徹底併發和更新的所指望可調整併發的哈希表。此類遵照與 Hashtable
相同的功能規範,而且包括對應於 Hashtable 的每一個方法的方法版本。不過,儘管全部操做都是線程安全的,但獲取操做不 必鎖定,而且不 支持以某種防止全部訪問的方式鎖定整個表。此類能夠經過程序徹底與 Hashtable 進行互操做,這取決於其線程安全,而與其同步細節無關。
獲取操做(包括 get)一般不會受阻塞,所以,可能與更新操做交迭(包括 put 和 remove)。獲取會影響最近完成的更新操做的結果。對於一些聚合操做,好比 putAll 和 clear,併發獲取可能隻影響某些條目的插入和移除。相似地,在建立迭代器/枚舉時或自此以後,Iterators 和 Enumerations 返回在某一時間點上影響哈希表狀態的元素。它們不會拋出 ConcurrentModificationException
。不過,迭代器被設計成每次僅由一個線程使用。
這容許經過可選的 concurrencyLevel 構造方法參數(默認值爲 16)來引導更新操做之間的併發,該參數用做內部調整大小的一個提示。表是在內部進行分區的,試圖容許指示無爭用併發更新的數量。由於哈希表中的位置基本上是隨意的,因此實際的併發將各不相同。理想狀況下,應該選擇一個儘量多地容納併發修改該表的線程的值。使用一個比所須要的值高不少的值可能會浪費空間和時間,而使用一個顯然低不少的值可能致使線程爭用。對數量級估計太高或估計太低一般都會帶來很是顯著的影響。當僅有一個線程將執行修改操做,而其餘全部線程都只是執行讀取操做時,才認爲某個值是合適的。此外,從新調整此類或其餘任何種類哈希表的大小都是一個相對較慢的操做,所以,在可能的時候,提供構造方法中指望表大小的估計值是一個好主意。