Java 集合系列之五:Map基本操做

1. Java Map

1. Java Map 重要觀點

  • Java Map接口是Java Collections Framework的成員。可是它不是Collection
  • 將鍵映射到值的對象。一個映射不能包含重複的鍵;每一個鍵最多隻能映射到一個值。(不一樣的鍵對應的值能夠相等)
  • Map 接口提供三種collection 視圖,容許以鍵集、值集或鍵-值映射關係集的形式查看某個映射的內容。
  • Map中某些映射實現可明確保證其天然順序和定製順序,如 TreeMap 類;另外一些映射實現則不保證任何順序,如 HashMap 類;還有些類保證添加順序。
  • 某些映射實現對可能包含的鍵和值有所限制。例如,某些實現禁止 null 鍵和值,另外一些則對其鍵的類型有限制。

2. Java Map類圖

一些最經常使用的Map實現類是HashMap,LinkedHashMap,TreeMap,SortedMap,HashTable,WeakedHashMap。html

Set的實現類都是基於Map來實現的(如,HashSet是經過HashMap實現的,TreeSet是經過TreeMap實現的,LinkedHashSet是經過LinkedHashMap來實現的)。 java

 

3. Java Map 方法

 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 視圖。

2. HashMap

1. HashMap 結構圖

 

 

在JDK1.6,JDK1.7中,HashMap採用位桶+鏈表實現,即便用鏈表處理衝突,同一hash值的鏈表都存儲在一個鏈表裏。可是當位於一個桶中的元素較多,即hash值相等的元素較多時,經過key值依次查找的效率較低。而JDK1.8中,HashMap採用位桶+鏈表+紅黑樹實現,當鏈表長度超過閾值(8)時,將鏈表轉換爲紅黑樹,這樣大大減小了查找時間。node

HashMap 繼承了AbstractMap,實現了Map<K,V>、Cloneable和Serializable接口!面試

  • 實現了Cloneable接口,即覆蓋了函數clone(),實現淺拷貝。
  • 實現了Serializable接口,支持序列化,可以經過序列化傳輸。

2. HashMap 重要特色

  1. HashMap 是一個散列表,它存儲的內容是鍵值對(key-value)映射。能夠存入null鍵,null值
  2. 底層實現即 (數組 + 單鏈表 + 紅黑樹),HashMap的底層是個Node數組(Node<K,V>[] table),在數組的具體索引位置,若是存在多個節點,則多是以鏈表或紅黑樹的形式存在。Node實現了Map.Entry接口,本質上是一個映射(k-v)
  3. DEFAULT_INITIAL_CAPACITY = 1 << 4; // 默認初始容量爲16,0000 0001 左移4位 0001 0000爲16,主幹數組的初始容量爲16,並且這個數組必須是2的倍數。
  4. MAXIMUM_CAPACITY = 1 << 30;//最大容量爲int的最大值除2
  5. DEFAULT_LOAD_FACTOR = 0.75f;//默認加載因子爲0.75,負載因子能夠大於1,即當元素個數超過容量長度的0.75倍時,進行擴容。經過調節負載因子,可以使 HashMap 時間和空間複雜度上有不一樣的表現。當咱們調低負載因子時,HashMap 所能容納的鍵值對數量變少。擴容時,從新將鍵值對存儲新的桶數組裏,鍵的鍵之間產生的碰撞會降低,鏈表長度變短。此時,HashMap 的增刪改查等操做的效率將會變高,這裏是典型的拿空間換時間。相反,若是增長負載因子(負載因子能夠大於1),HashMap 所能容納的鍵值對數量變多,空間利用率高,但碰撞率也高。這意味着鏈表長度變長,效率也隨之下降,這種狀況是拿時間換空間。
  6. TREEIFY_THRESHOLD = 8; //閾值,在jdk8中,HashMap處理「碰撞」增長了紅黑樹這種數據結構,若是主幹數組上的鏈表的長度大於8,鏈表轉化爲紅黑樹
  7. UNTREEIFY_THRESHOLD = 6; //hash表擴容後,若是發現某一個紅黑樹的長度小於6,則會從新退化爲鏈表
  8. MIN_TREEIFY_CAPACITY = 64; //當hashmap容量大於64時,鏈表才能轉成紅黑樹
  9. threshold;//即觸發擴容的閾值,臨界值=主幹數組容量*負載因子
  10. 解決衝突,鏈地址法(也叫拉鍊法)。jdk1.7中,當衝突時,在衝突的地址上生成一個鏈表,將衝突的元素的key,經過equals進行比較,相同即覆蓋,不一樣則添加到鏈表上,此時若是鏈表過長,效率就會大大下降,查找和添加操做的時間複雜度都爲O(n);可是在jdk1.8中若是鏈表長度大於8,鏈表就會轉化爲紅黑樹,時間複雜度也降爲了O(logn),性能獲得了很大的優化。
  11. 非同步,線程不安全,存取速度快(同步封裝Map m = Collections.synchronizedMap(new HashMap(...));),在併發場景下使用ConcurrentHashMap來代替。
  12. 擴容增量:原容量的 1 倍,閾值會變爲原來的2倍,如 HashSet的容量爲16,一次擴容後是容量爲32
  13. 擴容機制:1. 計算新桶數組的容量 newCap 和新閾值 newThr;2.根據計算出的 newCap 建立新的桶數組,桶數組 table 也是在這裏進行初始化的;3.將鍵值對節點從新映射到新的桶數組裏。若是節點是 TreeNode 類型,則須要拆分成黑樹。若是是普通節點,則節點按原順序進行分組。【從新映射紅黑樹的邏輯和從新映射鏈表的邏輯基本一致。不一樣的地方在於,從新映射後,會將紅黑樹拆分紅兩條由 TreeNode 組成的鏈表。若是鏈表長度小於 UNTREEIFY_THRESHOLD,則將鏈表轉換成普通鏈表。不然根據條件從新將 TreeNode 鏈表樹化。紅黑樹中仍然保留了原鏈表節點順序。有了這個前提,再將紅黑樹轉成鏈表就簡單多了,僅需將 TreeNode 鏈表轉成 Node 類型的鏈表便可。】
  14. 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。】算法

  15. HashMap在JDK1.8以後再也不有死循環的問題,JDK1.8以前存在死循環的根本緣由是在擴容後同一索引位置的節點順序會反掉。JDK1.8 從新映射後,兩條鏈表中的節點順序並未發生變化,仍是保持了擴容前的順序。
  16. get(key)1.判斷表或key是不是null,若是是直接返回null;2.獲取key的hash值,計算hash&(table.length-1)獲得在鏈表數組中的位置first=tab[hash&(table.length -1)],判斷索引處第一個key與傳入key是否相等,若是相等直接返回;3.若是不相等,判斷鏈表是不是紅黑二叉樹,若是是,直接從樹中取值;4.若是不是樹,就遍歷鏈表查找。
  17. put(key,value)的過程:1. 當桶數組 table 爲空或者null時,不然以默認大小resize();2.根據鍵值key計算hash值獲得插入的數組索引i,若是tab[i]==null,直接新建節點添加,不然判斷當前數組中處理hash衝突的方式爲鏈表仍是紅黑樹(check第一個節點類型便可),分別處理;3. 查找要插入的鍵值對已經存在,存在的話根據條件判斷是否用新值替換舊值;4.若是不存在,則將鍵值對鏈入鏈表中,並根據鏈表長度決定是否將鏈表轉爲紅黑樹;5.判斷鍵值對數量是否大於閾值,大於的話則進行擴容操做編程

  18. HasMap的擴容機制resize():構造hash表時,若是不指明初始大小,默認大小爲16(即Node數組大小16),若是Node[]數組中的元素達到(填充比*Node.length)從新調整HashMap大小 變爲原來2倍大小,擴容很耗時segmentfault

  19. HashMap有threshold屬性和loadFactor屬性,可是沒有capacity屬性。初始化時,若是傳了初始化容量值,該值是存在threshold變量,而且Node數組是在第一次put時纔會進行初始化,初始化時會將此時的threshold值做爲新表的capacity值,而後用capacity和loadFactor計算新表的真正threshold值。api

  20. 重寫計算hash是經過key的hashCode的高16位和低16位異或後和桶的數量取模獲得索引位置,即key.hashcode()^(hashcode>>>16)%length,;好處:1.讓高位數據與低位數據進行異或,以此加大低位信息的隨機性,變相的讓高位數據參與到計算中。2. 能夠增長 hash 的複雜度,進而影響 hash 的分佈性。這也就是爲何 HashMap 不直接使用鍵對象原始 hash 的緣由了。在 Java 中,hashCode 方法產生的 hash 是 int 類型,32 位寬。前16位爲高位,後16位爲低位,因此要右移16位。數組

  21. 當同一個索引位置的節點在增長後達到9個時,而且此時數組的長度大於等於64,則會觸發鏈表節點(Node)轉紅黑樹節點(TreeNode,間接繼承Node),轉成紅黑樹節點後,其實鏈表的結構還存在,經過next屬性維持。鏈表節點轉紅黑樹節點的具體方法爲源碼中的treeifyBin(Node<K,V>[] tab, int hash)方法。而若是數組長度小於64,則不會觸發鏈表轉紅黑樹,而是會進行擴容。緩存

  22. 當同一個索引位置的節點在移除後達到6個時,而且該索引位置的節點爲紅黑樹節點,會觸發紅黑樹節點轉鏈表節點。紅黑樹節點轉鏈表節點的具體方法爲源碼中的untreeify(HashMap<K,V> map)方法。

  23. 保證鍵的惟一性,須要覆蓋hashCode方法,和equals方法。先寫hashCode再寫equals   一、若是兩個對象相同(即用equals比較返回true),那麼它們的hashCode值必定要相同;二、若是兩個對象的hashCode相同,它們並不必定相同(即用equals比較返回false)  【由於equals()方法只比較兩個對象是否相同,至關於==,而不一樣的對象hashCode()確定是不一樣,因此若是咱們不是看對象,而只看對象的屬性,則要重寫這兩個方法,如Integer和String他們的equals()方法都是重寫過了,都只是比較對象裏的內容。使用HashMap,若是key是自定義的類,默認的equal函數的行爲可能不能符合咱們的要求,就必須重寫hashcode()和equals()。】

  24. 序列化:桶數組 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 繼續操做,就會出現問題。)】

  25. fail-fast機制:HashSet經過iterator()返回的迭代器是fail-fast的。
  26. 四種遍歷方法:map.keySet().iterator()map.entrySet().iterator();  foreach map.keySet(); foreach map.entrySet()
  27. 注意containsKey方法和containsValue方法。前者直接能夠經過key的哈希值將搜索範圍定位到指定索引對應的鏈表,然後者要對哈希數組的每一個鏈表進行搜索。

3. TreeMap

1. TreeMap 結構圖

基於紅黑樹(Red-Black tree)的 NavigableMap 實現。該映射根據其鍵的天然順序進行排序,或者根據建立映射時提供的 Comparator 進行排序,具體取決於使用的構造方法。

  此實現爲 containsKeygetput 和 remove 操做提供受保證的 log(n) 時間開銷。  

  TreeMap會自動排序,若是存放的對象不能排序則會報錯,因此存放的對象必須指定排序規則。排序規則包括天然排序和客戶排序。

  ①天然排序:TreeMap要添加哪一個對象就在哪一個對象類上面實現java.lang.Comparable接口,而且重寫comparaTo()方法,返回0則表示是同一個對象,不然爲不一樣對象。

  ②客戶排序:創建一個第三方類並實現java.util.Comparator接口。並重寫方法。定義集合形式爲TreeMap tm = new TreeMap(new 第三方類());


TreeMap繼承了AbstractMap,實現了NavigableMap、Cloneable和Serializable接口!

  • 繼承於AbstractMap,AbstractMap實現了equals和hashcode方法。
  • 實現了NavigableMap接口,意味着它支持一系列的導航方法。好比查找與指定目標最匹配項。
  • 實現了Cloneable接口,即覆蓋了函數clone(),實現淺拷貝。
  • 實現了Serializable接口,支持序列化,可以經過序列化傳輸。
  • TreeMap是SortedMap接口的實現類

2. TreeMap 重要特色

  1. 自平衡紅黑二叉樹,複雜度爲O(log (n))
  2. key支持2種排序方式:1 key 要實現Comparable接口 ,2 定製比較器 Comparator
  3. TreeMap是非同步的方法【SortedMap m = Collections.synchronizedSortedMap(new TreeMap(...));】。
  4. 它的iterator 方法返回的迭代器是fail-fast的。
  5. TreeMap的查詢、插入、刪除效率均沒有HashMap高,通常只有要對key排序時才使用TreeMap。
  6. TreeMap的key不能爲null,而HashMap的key能夠爲null。
  7. Entry是紅黑數的節點,它包含了紅黑數的6個基本組成成分:key(鍵)、value(值)、left(左孩子)、right(右孩子)、parent(父節點)、color(顏色)。
  8. TreeSet不支持快速隨機遍歷,只能經過迭代器進行遍歷! 兩種遍歷方法:Iterator【map.entrySet().iterator(); map.keySet().iterator();map.values();】,forEach

4. LinkedHashMap

1. LinkedHashMap 結構圖

LinkedHashMap類:LinkedHashMap正好介於HashMap和TreeMap之間,它也是一個hash表,但它同時維護了一個雙鏈表來記錄插入的順序,基本方法的複雜度爲O(1)。

當遍歷該集合時候,LinkedHashMap將會以元素的添加順序訪問集合的元素。

 

2. LinkedHashMap 重要特色

  1. 繼承自HashMap,((數組+鏈表+紅黑樹)+雙向鏈表)。
  2.  LinkedHashMap在迭代訪問Map中的所有元素時,性能比HashMapt好,可是插入時性能稍微遜色於HashMap。
  3. 非同步,線程不安全,存取速度快(同步封裝Map m = Collections.synchronizedMap(new LinkedHashMap(...));)
  4. 維護插入順序,從近期訪問最少到近期訪問最多的順序(訪問順序)。這種映射很適合構建 LRU 緩存。
  5. 此實現可讓客戶避免未指定的、由 HashMap(及 Hashtable)所提供的一般爲雜亂無章的排序工做,同時無需增長與 TreeMap 相關的成本。使用它能夠生成一個與原來順序相同的映射副本,而與原映射的實現無關。【Map copy = new LinkedHashMap(m);】
  6. LinkedHashMap的每個鍵值對都是經過內部的靜態類Entry<K, V>實例化的。這個 Entry<K, V>類繼承了HashMap.Entry類。這個靜態類增長了兩個成員變量,before和after來維護LinkedHasMap元素的插入順序。這兩個成員變量分別指向前一個和後一個元素,這讓LinkedHashMap也有相似雙向鏈表的表現。
  7. 它具備HashMap的全部特性,一樣容許key和value爲null。
  8. LinkedHashMap是如何實現LRU的。首先,當accessOrder爲true時,纔會開啓按訪問順序排序的模式,才能用來實現LRU算法。咱們能夠看到,不管是put方法仍是get方法,都會致使目標Entry成爲最近訪問的Entry,所以便把該Entry加入到了雙向鏈表的末尾(get方法經過調用recordAccess方法來實現,put方法在覆蓋已有key的狀況下,也是經過調用recordAccess方法來實現,在插入新的Entry時,則是經過createEntry中的addBefore方法來實現),這樣便把最近使用了的Entry放入到了雙向鏈表的後面,屢次操做後,雙向鏈表前面的Entry即是最近沒有使用的,這樣當節點個數滿的時候,刪除的最前面的Entry(head後面的那個Entry)即是最近最少使用的Entry。

5. HashTable

1. HashTable 結構圖

此類實現一個哈希表,該哈希表將鍵映射到相應的值。任何非 null 對象均可以用做鍵或值。

爲了成功地在哈希表中存儲和獲取對象,用做鍵的對象必須實現 hashCode 方法和 equals 方法。

2. HashTable 重要特色

  1. 實現一個哈希表(數組+鏈表),該哈希表將鍵映射到相應的值。任何非 null 對象均可以用做鍵或值。初始時已經構建了數據結構是Entry類型的數組,Entry源碼和hashmap基本元素用的node基本是同樣的
  2. 爲了成功地在哈希表中存儲和獲取對象,用做鍵的對象必須實現 hashCode 方法和 equals 方法。
  3. 默認加載因子(.75)在時間和空間成本上尋求一種折衷。加載因子太高雖然減小了空間開銷,但同時也增長了查找某個條目的時間(在大多數 Hashtable 操做中,包括 get 和 put 操做,都反映了這一點)。
  4. 初始容量主要控制空間消耗與執行 rehash 操做所須要的時間損耗之間的平衡。若是初始容量大於 Hashtable 所包含的最大條目數除以加載因子,則永遠 不會發生 rehash 操做。可是,將初始容量設置過高可能會浪費空間。若是不少條目要存儲在一個 Hashtable 中,那麼與根據須要執行自動 rehashing 操做來增大表的容量的作法相比,使用足夠大的初始容量建立哈希表或許能夠更有效地插入條目。
  5. 由全部類的「collection 視圖方法」返回的 collection 的 iterator 方法返回的迭代器都是快速失敗 的
  6. 由 Hashtable 的鍵和元素方法返回的 Enumeration  是快速失敗的。(保留是爲了兼容)
  7. 同步的,線程安全的,
  8. HashTable在不指定容量的狀況下的默認容量爲11,而HashMap爲16,Hashtable不要求底層數組的容量必定要爲2的整數次冪,而HashMap則要求必定爲2的整數次冪。
  9. Hashtable中key和value都不容許爲null,而HashMap中key和value都容許爲null(key只能有一個爲null,而value則能夠有多個爲null)。可是若是在Hashtable中有相似put(null,null)的操做,編譯一樣能夠經過,由於key和value都是Object類型,但運行時會拋出NullPointerException異常,這是JDK的規範規定的。

  10. Hashtable擴容時,將容量變爲原來的2倍加1,而HashMap擴容時,將容量變爲原來的2倍。【.關於2n+1的擴展,在hashtable選擇用取模的方式來進行,那麼儘可能使用素數、奇數會讓結果更加均勻一些,hashmap用2的冪,主要是其還有一個hash過程即二次hash,不是直接用key的hashcode,這個過程打散了數據整體就是一個減小hash衝突,而且找索引效率還要高,實現都是要考量這兩因素的】
  11. Hashtable計算hash值,直接用key的hashCode(),而HashMap從新計算了key的hash值,Hashtable在求hash值對應的位置索引時,用取模運算,而HashMap在求位置索引時,則用與運算,且這裏通常先用hash&0x7FFFFFFF後,再對length取模,&0x7FFFFFFF的目的是爲了將負的hash值轉化爲正值,由於hash值有可能爲負數,而&0x7FFFFFFF後,只有符號外改變,然後面的位都不變。

  12. hashtable已經算是廢棄了,從實現上看,實際hashmap比hashtable改進良多,無論hash方案,仍是結構上多紅黑樹,惟一缺點是非線程安全。可是hashtable的線程安全機制效率是很是差的,如今能找到很是多的替代方案,好比Collections.synchronizedMap,courrenthashmap等
  13. 遍歷方法: table.entrySet().iterator();table.keySet().iterator();Enumeration enu = table.keys();

4. WeakedHashMap

1. WeakedHashMap 結構圖

 

弱鍵 實現的基於哈希表的 Map。在 WeakHashMap 中,當某個鍵再也不正常使用時,將自動移除其條目。更精確地說,對於一個給定的鍵,其映射的存在並不阻止垃圾回收器對該鍵的丟棄,這就使該鍵成爲可終止的,被終止,而後被回收。丟棄某個鍵時,其條目從映射中有效地移除,所以,該類的行爲與其餘的 Map 實現有所不一樣。

2. WeakedHashMap 重要特色

  1. WeakHashMap 也是一個散列表,它存儲的內容也是鍵值對(key-value)映射,並且鍵和值均可以是null。
  2. WeakHashMap和HashMap都是經過"拉鍊法"實現的散列表。
  3. modCount是用來實現fail-fast機制的
  4. queue保存的是「已被GC清除」的「弱引用的鍵」。
  5. WeakHashMap是不一樣步的。可使用 Collections.synchronizedMap 方法來構造同步的 WeakHashMap。
  6. 垃圾回收機制經過WeakReference和ReferenceQueue實現的。 WeakHashMap的key是「弱鍵」,便是WeakReference類型的;ReferenceQueue是一個隊列,它會保存被GC回收的「弱鍵」。在每次get或者put的時候都會調用一個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」對應的鍵值對。
  7. 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,不會形成內存空間的浪費。

7. EntrySet vs KeySet

1. 遍歷

遍歷Map,並獲取其 <Key, Value> 的方法有兩種:

(1)KeySet<KeyType>

(2)EntrySet<KeyType, VlaueType>(性能更好)  

EntrySet速度比KeySet快了兩倍多點;

  • hashmap.entryset,在set集合中存放的是entry對象。而在hashmap中的key 和 value 是存放在entry對象裏面的;而後用迭代器,遍歷set集合,就能夠拿到每個entry對象;獲得entry對象就能夠直接從entry拿到value了;
  • hashmap.keyset,只是把hashmap中key放到一個set集合中去,仍是經過迭代器去遍歷,而後再經過 hashmap.get(key)方法拿到value; hashmap.get(key)方法內部調用的是getEntry(key),獲得entry,再從entry拿到value;
  • entry.getvalue能夠直接拿到value,hashmap.get(key)是先獲得Entry對象,再經過entry.getvalue去拿,直白點說就是hashmap.get(key)走了一個彎路,因此它慢一些;
  • keySet()的速度比entrySet()慢了不少,由於對於keySet實際上是遍歷了2次,一次是轉爲iterator,一次就從hashmap中取出key所對於的value。而entryset只是遍歷了第一次,他把key和value都放到了entry中,因此就快了

  差異在哪裏呢? 源碼給咱們答案了。

public V get(Object key) {
    if (key == null)
    return getForNullKey();
    Entry<K,V> entry = getEntry(key);
    return null == entry ? null : entry.getValue();
}

2. 應用

(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,近期最少使用算法),屢次遍歷速度會逐漸增長(直到寄存器被佔滿),由於經常使用數據會從主存被緩存到寄存器中。

3. 底層原理

  keySet()方法返回一個引用,這個引用指向了HashMap的一個內部類KeySet類,此內部類繼承了AbstractSet,此內部類初始化迭代器產生一個迭代器對象KeyIterator,它繼承了HashIterator迭代器,HashIterator迭代器初始化拿到了next指向map中的第一個元素。當使用keySet集合遍歷key時,實際上是使用迭代器KeyIterator迭代每一個節點的key。
  entrySet()方法同理。

4. 其餘

  • 對集合進行的for/in操做,最後會被編譯器轉化爲Iterator操做。可是使用for/in時,Iterator是不可見的,因此若是須要調用Iterator.remove()方法,或其餘一些操做, for/in循環就有些力不從心了。
  • Java中不存在foreach關鍵字,foreach是for/in的簡稱。
  • for循環比while循環節約內存空間,由於迭代器在for循環中,循環結束,迭代器屬於局部變量,循環結束就消失了,while循環中迭代器對象雖然也是局部變量可是要等方法運行完畢才能在內存中消失
  • 當循環次數比較多時,while循環理論上要比for循環要高效,由於for循環比while多一條彙編語句
 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 }
View Code

 

8. ConcurrentSkipListMap(JUC)

1. ConcurrentSkipListMap 結構圖

C

2. ConcurrentSkipListMap 重要特色

  1. 可縮放的併發 ConcurrentNavigableMap 實現。映射能夠根據鍵的天然順序進行排序,也能夠根據建立映射時所提供的 Comparator 進行排序,具體取決於使用的構造方法。

  2. 此類實現 SkipLists 的併發變體,爲 containsKeygetputremove 操做及其變體提供預期平均 log(n) 時間開銷。多個線程能夠安全地併發執行插入、移除、更新和訪問操做。迭代器是弱一致 的,返回的元素將反映迭代器建立時或建立後某一時刻的映射狀態。它們 拋出 ConcurrentModificationException能夠併發處理其餘操做。升序鍵排序視圖及其迭代器比降序鍵排序視圖及其迭代器更快。

  3. 此類及此類視圖中的方法返回的全部 Map.Entry 對,表示他們產生時的映射關係快照。它們 支持 Entry.setValue 方法。(注意,根據所需效果,可使用 putputIfAbsent 或 replace 更改關聯映射中的映射關係。)

  4. 請注意,與在大多數 collection 中不一樣,這裏的 size 方法不是 一個固定時間 (constant-time) 操做。由於這些映射的異步特性,肯定元素的當前數目須要遍歷元素。此外,批量操做 putAllequals 和 clear 並不 保證能以原子方式 (atomically) 執行。例如,與 putAll 操做一塊兒併發操做的迭代器只能查看某些附加元素。

9. ConcurrentHashMap(JUC)

1. ConcurrentHashMap 結構圖

 

2. ConcurrentHashMap 重要特色

  1. 支持獲取的徹底併發和更新的所指望可調整併發的哈希表。此類遵照與 Hashtable 相同的功能規範,而且包括對應於 Hashtable 的每一個方法的方法版本。不過,儘管全部操做都是線程安全的,但獲取操做 必鎖定,而且 支持以某種防止全部訪問的方式鎖定整個表。此類能夠經過程序徹底與 Hashtable 進行互操做,這取決於其線程安全,而與其同步細節無關。

     

  2. 獲取操做(包括 get)一般不會受阻塞,所以,可能與更新操做交迭(包括 put 和 remove)。獲取會影響最近完成的更新操做的結果。對於一些聚合操做,好比 putAll 和 clear,併發獲取可能隻影響某些條目的插入和移除。相似地,在建立迭代器/枚舉時或自此以後,Iterators 和 Enumerations 返回在某一時間點上影響哈希表狀態的元素。它們不會拋出 ConcurrentModificationException。不過,迭代器被設計成每次僅由一個線程使用。

     

  3. 這容許經過可選的 concurrencyLevel 構造方法參數(默認值爲 16)來引導更新操做之間的併發,該參數用做內部調整大小的一個提示。表是在內部進行分區的,試圖容許指示無爭用併發更新的數量。由於哈希表中的位置基本上是隨意的,因此實際的併發將各不相同。理想狀況下,應該選擇一個儘量多地容納併發修改該表的線程的值。使用一個比所須要的值高不少的值可能會浪費空間和時間,而使用一個顯然低不少的值可能致使線程爭用。對數量級估計太高或估計太低一般都會帶來很是顯著的影響。當僅有一個線程將執行修改操做,而其餘全部線程都只是執行讀取操做時,才認爲某個值是合適的。此外,從新調整此類或其餘任何種類哈希表的大小都是一個相對較慢的操做,所以,在可能的時候,提供構造方法中指望表大小的估計值是一個好主意。

抄錄網址

  1. 高效編程之HashMap的entryset和keyset比較
  2. HashMap的keySet()和entrySet()實現原理
  3. Java 遍歷Map的2種方法(KeySet、EntrySet)
  4. map集合的keySet和entrySet
  5. Java常見集合的默認大小及擴容機制
  6. Java集合源碼剖析
  7. java集合系列——Map介紹(七)
  8. Java集合系列專欄
  9. http://tool.oschina.net/apidocs/apidoc?api=jdk_7u4
  10. http://tool.oschina.net/apidocs/apidoc?api=jdk-zh
  11. Java集合:HashMap詳解(JDK 1.8)
  12. HashMap 源碼詳細分析(JDK1.8)
  13. HashMap JDK1.8原理分析
  14. 【Java集合源碼剖析】HashTable源碼剖析
  15. Java 集合系列13之 WeakHashMap詳細介紹(源碼解析)和使用示例
  16. WeakHashMap的使用場景
  17. Java7/8 中的 HashMap 和 ConcurrentHashMap 全解析
  18. HashMap?面試?我是誰?我在哪
  19. HashMap併發致使死循環 CurrentHashMap
  20. 高併發編程系列:ConcurrentHashMap的實現原理(JDK1.7和JDK1.8)
  21. Java7/8 中的 HashMap 和 ConcurrentHashMap 全解析
  22. jdk8之HashMap resize方法詳解(深刻講解爲何1.8中擴容後的元素新位置爲原位置+原數組長度)
  23. 深刻理解 HashMap put 方法(JDK 8逐行剖析)
  24. HashMap1.8中多線程擴容引發的死循環問題
  25. 多線程-ConcurrentHashMap(JDK1.8)
  26. jdk1.6及1.8 HashMap線程安全分析
  27. HashMap1.8源碼分析及線程安全性問題的分析
  28. 淺談HashMap與線程安全 (JDK1.8)
相關文章
相關標籤/搜索