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。

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

 

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)時,將鏈表轉換為紅黑樹,這樣大大減少了查找時間。

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倍大小,擴容很耗時

  19. HashMap有threshold屬性和loadFactor屬性,但是沒有capacity屬性。初始化時,如果傳了初始化容量值,該值是存在threshold變量,並且Node數組是在第一次put時才會進行初始化,初始化時會將此時的threshold值作為新表的capacity值,然后用capacity和loadFactor計算新表的真正threshold值。

  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   1、如果兩個對象相同(即用equals比較返回true),那么它們的hashCode值一定要相同;2、如果兩個對象的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)


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM