Map,HashMap,TreeMap
1. Map
Map接口中,鍵和值一一映射,可以通過鍵來獲取值。
在數組中我們是通過數組下標來對其內容索引的,而在Map中我們通過對象來對對象進行索引,用來索引的對象叫做key,其對應的對象叫做value。這就是我們平時說的鍵值對。
Map接口的方法
| 1 | void clear( ) ?從此映射中移除所有映射關系(可選操作)。 |
| 2 | boolean containsKey(Object k) 如果此映射包含指定鍵的映射關系,則返回?true。 |
| 3 | boolean containsValue(Object v) 如果此映射將一個或多個鍵映射到指定值,則返回?true。 |
| 4 | Set entrySet( ) 返回此映射中包含的映射關系的?Set 視圖。 |
| 5 | boolean equals(Object obj) 比較指定的對象與此映射是否相等。 |
| 6 | Object get(Object k) 返回指定鍵所映射的值;如果此映射不包含該鍵的映射關系,則返回?null。 |
| 7 | int hashCode( ) 返回此映射的哈希碼值。 |
| 8 | boolean isEmpty( ) 如果此映射未包含鍵-值映射關系,則返回?true。 |
| 9 | Set keySet( ) 返回此映射中包含的鍵的?Set 視圖。 |
| 10 | Object put(Object k, Object v) 將指定的值與此映射中的指定鍵關聯(可選操作)。 |
| 11 | void putAll(Map m) 從指定映射中將所有映射關系復制到此映射中(可選操作)。 |
| 12 | Object remove(Object k) 如果存在一個鍵的映射關系,則將其從此映射中移除(可選操作)。 |
| 13 | int size( ) 返回此映射中的鍵-值映射關系數。 |
| 14 | Collection values( ) 返回此映射中包含的值的 Collection 視圖。 |
下面的關于HashMap的原文地址:https://baijiahao.baidu.com/s?id=1618550070727689060&wfr=spider&for=pc
2. HashMap
HashMap的工作原理
HashMap是基于hashing的原理,我們使用put(key, value)存儲對象到HashMap中,使用get(key)從HashMap中獲取對象。當我們給put()方法傳遞鍵和值時,我們先對鍵調用hashCode()方法,計算并返回的hashCode是用于找到Map數組的bucket位置來儲存Node 對象。這里關鍵點在于指出,HashMap是在bucket中儲存鍵對象和值對象,作為Map.Node 。
a. 以下是HashMap初始化 ,簡單模擬數據結構
Node[] table=new Node[16] 散列桶初始化,tableclass Node { hash;//hash值 key;//鍵 value;//值 node next;//用于指向鏈表的下一層(產生沖突,用拉鏈法)}
b.以下是具體的put過程(JDK1.8版)
對Key求Hash值,然后再計算下標
如果沒有碰撞,直接放入桶中(碰撞的意思是計算得到的Hash值相同,需要放到同一個bucket中)
如果碰撞了,以鏈表的方式鏈接到后面
如果鏈表長度超過閥值( TREEIFY THRESHOLD==8),就把鏈表轉成紅黑樹,鏈表長度低于6,就把紅黑樹轉回鏈表
如果節點已經存在就替換舊值
如果桶滿了(容量16*加載因子0.75),就需要 resize(擴容2倍后重排)
c. 以下是具體get過程(考慮特殊情況如果兩個鍵的hashcode相同,你如何獲取值對象?)
當我們調用get()方法,HashMap會使用鍵對象的hashcode找到bucket位置,找到bucket位置之后,會調用keys.equals()方法去找到鏈表中正確的節點,最終找到要找的值對象。
有什么方法可以減少碰撞?
HashMap中hash函數怎么是是實現的?
我們可以看到在hashmap中要找到某個元素,需要根據key的hash值來求得對應數組中的位置。如何計算這個位置就是hash算法。前面說過hashmap的數據結構是數組和鏈表的結合,所以我們當然希望這個hashmap里面的元素位置盡量的分布均勻些,盡量使得每個位置上的元素數量只有一個,那么當我們用hash算法求得這個位置的時候,馬上就可以知道對應位置的元素就是我們要的,而不用再去遍歷鏈表。 所以我們首先想到的就是把hashcode對數組長度取模運算,這樣一來,元素的分布相對來說是比較均勻的。但是,“模”運算的消耗還是比較大的,能不能找一種更快速,消耗更小的方式,我們來看看JDK1.8的源碼是怎么做的(被樓主修飾了一下)
返回散列值也就是hashcode // ^ :按位異或 // >>>:無符號右移,忽略符號位,空位都以0補齊 //其中n是數組的長度,即Map的數組部分初始化長度 return (n-1)&(h ^ (h >>> 16));}
簡單來說就是
1、高16bt不變,低16bit和高16bit做了一個異或(得到的HASHCODE轉化為32位的二進制,前16位和后16位低16bit和高16bit做了一個異或)
2、(n·1)&hash=->得到下標
如果HashMap的大小超過了負載因子(load factor)定義的容量,怎么辦?
默認的負載因子大小為0.75,也就是說,當一個map填滿了75%的bucket時候,和其它集合類(如ArrayList等)一樣,將會創建原來HashMap大小的兩倍的bucket數組,來重新調整map的大小,并將原來的對象放入新的bucket數組中。這個過程叫作rehashing,因為它調用hash方法找到新的bucket位置。這個值只可能在兩個地方,一個是原下標的位置,另一種是在下標為<原下標+原容量>的位置
重新調整HashMap大小存在什么問題嗎?
當重新調整HashMap大小的時候,確實存在條件競爭,因為如果兩個線程都發現HashMap需要重新調整大小了,它們會同時試著調整大小。在調整大小的過程中,存儲在鏈表中的元素的次序會反過來,因為移動到新的bucket位置的時候,HashMap并不會將元素放在鏈表的尾部,而是放在頭部,這是為了避免尾部遍歷(tail traversing)。如果條件競爭發生了,那么就死循環了。(多線程的環境下不使用HashMap)
為什么多線程會導致死循環,它是怎么發生的?
HashMap的容量是有限的。當經過多次元素插入,使得HashMap達到一定飽和度時,Key映射位置發生沖突的幾率會逐漸提高。這時候,HashMap需要擴展它的長度,也就是進行Resize。1.擴容:創建一個新的Entry空數組,長度是原數組的2倍。2.ReHash:遍歷原Entry數組,把所有的Entry重新Hash到新數組。
HashMap為什么是線程不安全的?
HashMap 在并發時可能出現的問題主要是兩方面:
比如有兩個線程A和B,首先A希望插入一個key-value對到HashMap中,首先計算記錄所要落到的 hash桶的索引坐標,然后獲取到該桶里面的鏈表頭結點,此時線程A的時間片用完了,而此時線程B被調度得以執行,和線程A一樣執行,只不過線程B成功將記錄插到了桶里面,假設線程A插入的記錄計算出來的 hash桶索引和線程B要插入的記錄計算出來的 hash桶索引是一樣的,那么當線程B成功插入之后,線程A再次被調度運行時,它依然持有過期的鏈表頭但是它對此一無所知,以至于它認為它應該這樣做,如此一來就覆蓋了線程B插入的記錄,這樣線程B插入的記錄就憑空消失了,造成了數據不一致的行為。
這種情況發生在HashMap自動擴容時,當2個線程同時檢測到元素個數超過 數組大小 × 負載因子。此時2個線程會在put()方法中調用了resize(),兩個線程同時修改一個鏈表結構會產生一個循環鏈表(JDK1.7中,會出現resize前后元素順序倒置的情況)。接下來再想通過get()獲取某一個元素,就會出現死循環。
作者:小北覓
鏈接:https://www.jianshu.com/p/ee0de4c99f87
HashMap的三種遍歷方法:
package HashMap_demo;import java.util.HashMap; import java.util.Iterator; import java.util.Map; import java.util.Map.Entry;public class HashMap_demo {public static void main(String[] args) {//Map<Integer, String> map=new HashMap<>();//這個和底下的一個寫法都可以。Map<Integer, String> map=new HashMap<Integer, String>();map.put(1, "A");map.put(8, "D");map.put(3, "C");map.put(4, "D");map.put(3, "E");map.put(2, "");map.put(null, "C");//通過Map中的keySet()方法遍歷獲得key值進行輸出//看別的大佬都管這叫增強for循環遍歷System.out.println("通過Map中的keySet()方法遍歷獲得key值進行輸出:");for(Integer s:map.keySet()) {System.out.print("Key="+s+":");System.out.print("value="+map.get(s)+"--");}//通過Map中的keySet()方法與valueSet()方法遍歷獲得key值和value值進行輸出System.out.println("\n"+"通過Map中的keySet()方法與valueSet()方法遍歷獲得key值和value值進行輸出");for(Integer s:map.keySet()) {System.out.print("Key="+s+":");}for(String v:map.values()) {System.out.print("value="+v+":");}//使用Iterator迭代器迭代輸出System.out.println("\n"+"使用Iterator迭代器進行迭代輸出:");Iterator<Entry<Integer, String>> iter=map.entrySet().iterator();while(iter.hasNext()) {Entry<Integer, String> entry=iter.next();System.out.print("Key="+entry.getKey()+":");System.out.print("value="+entry.getValue()+"--");}}}運行結果:
通過Map中的keySet()方法遍歷獲得key值進行輸出: Key=null:value=C--Key=1:value=A--Key=2:value=--Key=3:value=E--Key=4:value=D--Key=8:value=D-- 通過Map中的keySet()方法與valueSet()方法遍歷獲得key值和value值進行輸出 Key=null:Key=1:Key=2:Key=3:Key=4:Key=8:value=C:value=A:value=:value=E:value=D:value=D: 使用Iterator迭代器進行迭代輸出: Key=null:value=C--Key=1:value=A--Key=2:value=--Key=3:value=E--Key=4:value=D--Key=8:value=D--注意:hashMap的遍歷輸出結果應該是無序的,我這里不知道為啥,一直是有序的…
下段代碼來自:https://www.cnblogs.com/biehongli/p/6443701.html 做了一點點小改動
package Map_demo;import java.util.Collection; import java.util.HashMap; import java.util.Map; import java.util.Set;public class HashMap_demo {public static void main(String[] args) {// 1:key,value都是object類型的// 2:key必須是唯一的,不唯一,那么后面的value會把前面的value覆蓋// 3:對于HashMap,key可以為空// 4:value可以為空,也可以為空// 5:HashTable的key和value不能為空// 6:properties的key和value必須為String類型的Map<String, String> map = new HashMap<>();map.put("null", "this is null 1");map.put("null", "this is null 2");map.put(null, "NO NUll");System.out.println(map.size());System.out.println(map.get("null"));System.out.println(map.get(null));System.out.println("=============================");// 循環顯示map類型的key以及對應的value// 三個集合,key的集合,value的集合,鍵值對的集合Set<String> keys = map.keySet();for (String s : keys) {System.out.println(s);}System.out.println("=============================");Collection<String> values = map.values();// 值的集合System.out.println(values);System.out.println("=============================");Set<Map.Entry<String, String>> entrys = map.entrySet();// 鍵值對的集合for (Map.Entry<String, String> entry : entrys) {System.out.println(entry.getKey() + " " + entry.getValue());}} }輸出結果:
2 this is null 2 NO NUll ============================= null null ============================= [NO NUll, this is null 2] ============================= null NO NUll null this is null 2總結:上段代碼說明了
TreeMap:https://mp.csdn.net/mdeditor/98071869#
HashMap與TreeMap的區別
a.)Map是接口,HashMap與TreeMap是類(即HashMap和TreeMap實現了Map的所有的方法)
b.)HashMap非線程安全,TreeMap線程安全
c.)HashMap:適用于在Map中插入、刪除和定位元素。
Treemap:適用于按自然順序或自定義順序遍歷鍵(key)。
d.)HashMap通常比TreeMap快一點(樹和哈希表的數據結構使然),建議多使用HashMap,在需要排序的Map時候才用TreeMap。
e.)HashMap的結果是沒有排序的,而TreeMap輸出的結果是排好序的。
HashMap和HashTable的區別
HashMap和Hashtable都實現了Map接口,但決定用哪一個之前先要弄清楚它們之間的分別。主要的區別有:線程安全性,同步(synchronization),以及速度。
HashMap幾乎可以等價于Hashtable,除了HashMap是非synchronized的,并可以接受null(HashMap可以接受為null的鍵值(key)和值(value),而Hashtable則不行)。
HashMap是非synchronized,而Hashtable是synchronized,這意味著Hashtable是線程安全的,多個線程可以共享一個Hashtable;而如果沒有正確的同步的話,多個線程是不能共享HashMap的。Java 5提供了ConcurrentHashMap,它是HashTable的替代,比HashTable的擴展性更好。
另一個區別是HashMap的迭代器(Iterator)是fail-fast迭代器,而Hashtable的enumerator迭代器不是fail-fast的。所以當有其它線程改變了HashMap的結構(增加或者移除元素),將會拋出ConcurrentModificationException,但迭代器本身的remove()方法移除元素則不會拋出ConcurrentModificationException異常。但這并不是一個一定發生的行為,要看JVM。這條同樣也是Enumeration和Iterator的區別。
由于Hashtable是線程安全的也是synchronized,所以在單線程環境下它比HashMap要慢。如果你不需要同步,只需要單一線程,那么使用HashMap性能要好過Hashtable。
HashMap不能保證隨著時間的推移Map中的元素次序是不變的。
作者:小北覓
鏈接:https://www.jianshu.com/p/ee0de4c99f87
ps:補充小知識
線程安全
在Java里,線程安全一般體現在兩個方面:
1、多個thread對同一個java實例的訪問(read和modify)不會相互干擾,它主要體現在關鍵字synchronized。如ArrayList和Vector,HashMap和Hashtable
(后者每個方法前都有synchronized關鍵字)。如果你在interator一個List對象時,其它線程remove一個element,問題就出現了。
2、每個線程都有自己的字段,而不會在多個線程之間共享。它主要體現在java.lang.ThreadLocal類,而沒有Java關鍵字支持,如像static、transient那樣。
總結
以上是生活随笔為你收集整理的Map,HashMap,TreeMap的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 牛客java面试题总结版(三)
- 下一篇: java集合类根接口:Collectio