HashMap,HashSet html
摘自:https://www.cnblogs.com/skywang12345/p/3310887.html#a1 java
目錄算法
1、 HashMap(鍵值對形式存取,鍵值不能相同) 2 編程
3. 疑問:若是兩個key經過hash%Entry[].length獲得的 index相同,會不會有覆蓋的危險? 4安全
4. 解決hash衝突的方法 5數據結構
3. HashSet源碼解析(基於JDK1.6.0_45) 8
2. 深刻理解ConcurrentHashMap原理分析即線程安全問題 18
1) ConcurrentHashMap與HashTable的區別 18
7、 HashMap,HashTable和ConcurrentHashMap的區別 21
1. HashMap與ConcurrentHashMap的區別 21
2. ConcurrentHashMap vs Hashtable vs Synchronized Map區別 21
數組的特色是:尋址容易,插入和刪除困難。
鏈表的特色是:尋址困難,插入和刪除容易。
綜合這二者的特性,獲得一種尋址容易,插入刪除也容易的數據結構:這就是咱們要提起的哈希表。
哈希表有多種不一樣的實現方法,咱們接下來解釋的是最經常使用的方法——拉鍊法,咱們能夠理解爲"鏈表的數組":如圖:
從上圖咱們能夠發現哈希表是由數組+鏈表組成的,一個長度爲16的數組中,每一個元素存儲的是一個鏈表的頭結點。那麼這些元素是按照什麼樣的規則存儲到數組中的呢?通常狀況下是經過hash(key)%len得到,也就是元素的key的哈希值對數組的長度取餘獲得。好比上述哈希表中,12%16=12,28%16=12,108%16=12,140%16=12。因此12,28,108,140都存儲在數組下標爲12的鏈表的位置。
HashMap其實也是一個線性的數組實現的,因此能夠理解爲其存儲數據的容器就是一個線性數組,這可能讓咱們很不解,一個線性的數據怎麼實現按鍵值對來存取數據呢?這裏HashMap有作一些處理。
首先HahsMap裏面實現了一個靜態內部類,其重要的屬性有key,value,next,從屬性key,value咱們就能很明顯的看出來Entry就是HashMap鍵值對實現的一個基礎bean,咱們上面說到HashMap的基礎就是一個線性數組,這個數組就是Entry[],Map裏面的內容都保存在Entry[]裏面。
既然是線性數組,爲何能隨機存取呢?這裏HashMap用了一個小算法,大體HashMap 採用一種所謂的"Hash 算法"來決定每一個元素的存儲位置。當程序執行 map.put(String,Obect)方法時,系統將調用String的 hashCode() 方法獲得其 hashCode 值——每一個 Java 對象都有 hashCode() 方法,均可經過該方法得到它的 hashCode 值。獲得這個對象的 hashCode 值以後,系統會根據該 hashCode 值來決定該元素的存儲位置。是這樣實現:
//存儲時: int hash = key.hashCode();// 這個hashCode方法這裏不詳述,只要理解每一個key的hash是一個固定的int值 int index = hash % Entry[].length; Entry[index] = value;
//取值時: int hash = key.hashCode(); int index = hash % Entry[].length; return Entry[index]; |
這裏的話咱們:
對於存儲:
對於取值:
這樣佔用的內存會很大
這裏HashMap裏面用到鏈式數據結構的一個概念。上面咱們提到過Entry類裏面有一個next屬性,做用是指向下一個Entry。打個比方,第一個鍵值對A進來,經過計算其key的hash獲得的index=0,記做:Entry[0]=A。一會又進來一個鍵值對B,經過計算其index也等於0;如今怎麼辦?HashMap會這樣作:B.next = A,Entry[0] = B,若是又進來C,index也等於0,那麼C.next = B,Entry[0] = C;這樣咱們發現index=0的地方其實存取了A,B,C三個鍵值對,他們經過next這個屬性連接在一塊兒。因此疑問不用擔憂。也就是說數組中存儲的是最後插入的元素。到這裏爲止,HashMap的大體實現,咱們應該已經清楚了。
固然HashMap裏面也包含一些優化方面的實現,這裏也說一下。好比:Entry[]的長度必定後,隨着map裏面數據的愈來愈長,這樣同一個index的鏈就會很長,會不會影響性能?HashMap裏面設置一個因素(也稱爲因子),隨着map的size愈來愈大,Entry[]會以必定的規則加長長度。
Java中hashMap的解決方法就是採用鏈地址法。
若兩個不相等的 key 產生了相等的哈希值,這時則須要採用哈希衝突。
首先,HashMap是由線性數組組成的,如今咱們假設初始數組的長度爲5;而後咱們存儲數據,假設存儲的第一個數據的鍵值的hashcode計算出來的值爲6,而後咱們經過hashcode計算出來的值與數組長度取餘獲得存儲第一個數據的下標,即6%5=1;當咱們存儲另外的數據,若是經過鍵值的hashcode計算出來的值是11,那麼此時計算出數據的下標11%5=1也是1。這就是哈希衝突。
Java採用拉鍊法解決哈希衝突。
HashMap<String, Integer> map = new HashMap<String, Integer>(); map.put("wang", 01); map.put("wang",02); System.out.println(map.get("wang")); System.out.println("----------------"); map.remove("wang"); System.out.println(map.get("wang")); |
*************** 2 ---------------- null |
方法:則直接更新該鍵的值
方法:將值插入到單鏈表的頭結點。
HashSet是一個沒有重複元素的集合,它是由HashMap實現的,不保證元素的順序,並且HashSet容許使用null元素。
HashSet是非同步的,若是多個線程同時訪問一個HashSet,而其中至少一個線程修改了該set,那麼它必須保持外部同步。這一般是經過對天然封裝該set的對象執行同步操做來完成的。若是不存在這樣的對象,則應該使用Collections.synchronizedSet方法來包裝set,最好在建立完成時完成這一操做,以防止對該set進行意外的不一樣步訪問:
Set s = Collections.synchronizedSet(new HashSet(...)); |
HashSet經過iterator()迭代器進行訪問。
HashSet的繼承關係以下:
java.lang.Object ↳ java.util.AbstractCollection<E> ↳ java.util.AbstractSet<E> ↳ java.util.HashSet<E>
public class HashSet<E> extends AbstractSet<E> implements Set<E>, Cloneable, java.io.Serializable { } |
從上圖能夠看出:
爲了更瞭解HashSet的原理,下面對HashSet源碼代碼做出分析。
package java.util;
public class HashSet<E> extends AbstractSet<E> implements Set<E>, Cloneable, java.io.Serializable { static final long serialVersionUID = -5024744406713321676L;
// HashSet是經過map(HashMap對象)保存內容的 private transient HashMap<E,Object> map;
// PRESENT是向map中插入key-value對應的value // 由於HashSet中只須要用到key,而HashMap是key-value鍵值對; // 因此,向map中添加鍵值對時,鍵值對的值固定是PRESENT private static final Object PRESENT = new Object();
// 默認構造函數 public HashSet() { // 調用HashMap的默認構造函數,建立map map = new HashMap<E,Object>(); }
// 帶集合的構造函數 public HashSet(Collection<? extends E> c) { // 建立map。 // 爲何要調用Math.max((int) (c.size()/.75f) + 1, 16),從 (c.size()/.75f) + 1 和 16 中選擇一個比較大的樹呢? // 首先,說明(c.size()/.75f) + 1 // 由於從HashMap的效率(時間成本和空間成本)考慮,HashMap的加載因子是0.75。 // 當HashMap的"閾值"(閾值=HashMap總的大小*加載因子) < "HashMap實際大小"時, // 就須要將HashMap的容量翻倍。 // 因此,(c.size()/.75f) + 1 計算出來的正好是總的空間大小。 // 接下來,說明爲何是 16 。 // HashMap的總的大小,必須是2的指數倍。若建立HashMap時,指定的大小不是2的指數倍; // HashMap的構造函數中也會從新計算,找出比"指定大小"大的最小的2的指數倍的數。 // 因此,這裏指定爲16是從性能考慮。避免重複計算。 map = new HashMap<E,Object>(Math.max((int) (c.size()/.75f) + 1, 16)); // 將集合(c)中的所有元素添加到HashSet中 addAll(c); }
// 指定HashSet初始容量和加載因子的構造函數 public HashSet(int initialCapacity, float loadFactor) { map = new HashMap<E,Object>(initialCapacity, loadFactor); }
// 指定HashSet初始容量的構造函數 public HashSet(int initialCapacity) { map = new HashMap<E,Object>(initialCapacity); }
HashSet(int initialCapacity, float loadFactor, boolean dummy) { map = new LinkedHashMap<E,Object>(initialCapacity, loadFactor); }
// 返回HashSet的迭代器 public Iterator<E> iterator() { // 實際上返回的是HashMap的"key集合的迭代器" return map.keySet().iterator(); }
public int size() { return map.size(); }
public boolean isEmpty() { return map.isEmpty(); }
public boolean contains(Object o) { return map.containsKey(o); }
// 將元素(e)添加到HashSet中 public boolean add(E e) { return map.put(e, PRESENT)==null; }
// 刪除HashSet中的元素(o) public boolean remove(Object o) { return map.remove(o)==PRESENT; }
public void clear() { map.clear(); }
// 克隆一個HashSet,並返回Object對象 public Object clone() { try { HashSet<E> newSet = (HashSet<E>) super.clone(); newSet.map = (HashMap<E, Object>) map.clone(); return newSet; } catch (CloneNotSupportedException e) { throw new InternalError(); } }
// java.io.Serializable的寫入函數 // 將HashSet的"總的容量,加載因子,實際容量,全部的元素"都寫入到輸出流中 private void writeObject(java.io.ObjectOutputStream s) throws java.io.IOException { // Write out any hidden serialization magic s.defaultWriteObject(); // Write out HashMap capacity and load factor s.writeInt(map.capacity()); s.writeFloat(map.loadFactor()); // Write out size s.writeInt(map.size()); // Write out all elements in the proper order. for (Iterator i=map.keySet().iterator(); i.hasNext(); ) s.writeObject(i.next()); } // java.io.Serializable的讀取函數 // 將HashSet的"總的容量,加載因子,實際容量,全部的元素"依次讀出 private void readObject(java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException { // Read in any hidden serialization magic s.defaultReadObject();
// Read in HashMap capacity and load factor and create backing HashMap int capacity = s.readInt(); float loadFactor = s.readFloat(); map = (((HashSet)this) instanceof LinkedHashSet ? new LinkedHashMap<E,Object>(capacity, loadFactor) : new HashMap<E,Object>(capacity, loadFactor)); // Read in size int size = s.readInt(); // Read in all elements in the proper order. for (int i=0; i<size; i++) { E e = (E) s.readObject(); map.put(e, PRESENT); } } } |
說明: HashSet的代碼實際上很是簡單,經過上面的註釋應該很可以看懂。它是經過HashMap實現的,若對HashSet的理解有困難,建議先學習如下HashMap;學完HashMap以後,在學習HashSet就很是容易了。
第一步:根據iterator()獲取HashSet的迭代器
遍歷迭代器獲取各個元素
// 假設set是HashSet對象 for(Iterator iterator = set.iterator(); iterator.hasNext(); ) { iterator.next(); } |
// 假設set是HashSet對象,而且set中元素是String類型 String[] arr = (String[])set.toArray(new String[0]); for (String str:arr) System.out.printf("for each : %s\n", str); |
向HashSet中添加元素,若是set中元素已存在,則返回false;若是不存在,則返回true。
TreeMap |
HashMap |
TreeMap實現了SortMap接口,是基於紅黑樹的 |
HashMap實現了Map接口,是基於哈希散列表的 |
TreeMap默認按鍵的升序排序 |
HashMap隨機存儲 |
TreeMap的遍歷是Iterator按順序遍歷的 |
HahsMap的遍歷是Iterator隨機遍歷的 |
TreeMap鍵和值都不能爲空 |
HashMap鍵只能有一個null,值能夠有多個null |
TreeMap插入刪除查找的效率比較低 |
HashMap插入刪除查找的效率比較高 |
非線程安全的 |
非線程安全的 |
在HashSet中,元素都存到HashMap鍵值對的Key上面,而Value時有一個統一的值private static final Object PRESENT = new Object();,
當有新值加入時,底層的HashMap會判斷Key值是否存在(HashMap細節請移步深刻理解HashMap),若是不存在,則插入新值,同時這個插入的細節會依照HashMap插入細節;若是存在就不插入
HashMap |
HashSet |
HashMap實現了Map接口 |
HashSet實現了Set接口 |
HashMap存儲鍵值對 |
HashSet僅僅存儲對象,存儲的是鍵,他們的值是相同的。 |
使用pub()方法將元素放入map中 |
使用add()方法將元素放入set中 |
HashMap中使用鍵對象來計算hashcode值(不會返回true和false) |
HashSet使用成員對象來計算hashcode值,對於兩個對象來講hashcode可能相同,因此equals()方法用來判斷對象的相等性,若是兩個對象不一樣的話,那麼返回false |
HashMap查找比較快,由於是使用惟一的鍵來獲取對象 |
HashSet較HashMap來講比較慢 |
HashMap |
HashTable |
HashMap是基於AbstractMap |
HashTable基於Dictionary類 |
HashMap能夠容許存在一個爲null的key和任意個爲null的value |
HashTable中的key和value都不容許爲null |
HashMap時單線程安全的,多線程是不安全的 |
Hashtable是多線程安全的 |
HashMap僅支持Iterator的遍歷方式 |
Hashtable支持Iterator和Enumeration兩種遍歷方式 |
Hashtable 的函數都是同步的,這意味着它是線程安全的。它的key、value都不能夠爲null。Hashtable的方法都用synchronized來修飾,因此它是線程同步的。
Hashtable的遍歷:
遍歷Hashtable的鍵值對
第一步:根據entrySet()獲取Hashtable的"鍵值對"的Set集合。
經過Iterator迭代器遍歷"第一步"獲得的集合。
// 假設table是Hashtable對象 // table中的key是String類型,value是Integer類型 Integer integ = null; Iterator iter = table.entrySet().iterator(); while(iter.hasNext()) { Map.Entry entry = (Map.Entry)iter.next(); // 獲取key key = (String)entry.getKey(); // 獲取value integ = (Integer)entry.getValue(); } |
經過Iterator遍歷Hashtable的鍵
第一步:根據keySet()獲取Hashtable的"鍵"的Set集合。
第二步:經過Iterator迭代器遍歷"第一步"獲得的集合。
// 假設table是Hashtable對象 // table中的key是String類型,value是Integer類型 String key = null; Integer integ = null; Iterator iter = table.keySet().iterator(); while (iter.hasNext()) { // 獲取key key = (String)iter.next(); // 根據key,獲取value integ = (Integer)table.get(key); } |
經過Iterator遍歷Hashtable的值
第一步:根據value()獲取Hashtable的"值"的集合。
第二步:經過Iterator迭代器遍歷"第一步"獲得的集合
// 假設table是Hashtable對象 // table中的key是String類型,value是Integer類型 Integer value = null; Collection c = table.values(); Iterator iter= c.iterator(); while (iter.hasNext()) { value = (Integer)iter.next(); } |
經過Enumeration遍歷Hashtable的鍵
第一步:根據keys()獲取Hashtable的集合。
第二步:經過Enumeration遍歷"第一步"獲得的集合
Enumeration enu = table.elements(); while(enu.hasMoreElements()) { System.out.println(enu.nextElement()); } |
經過Enumeration遍歷Hashtable的值
第一步:根據elements()獲取Hashtable的集合。
第二步:經過Enumeration遍歷"第一步"獲得的集合
Enumeration enu = table.elements(); while(enu.hasMoreElements()) { System.out.println(enu.nextElement()); } |
首先經常使用的三種HashMap包括HashMap,HashTable和concurrentHashMap:
HashTable 的put()源代碼
從代碼能夠看出來在全部put 的操做的時候都須要用 synchronized 關鍵字進行同步。而且key 不能爲空。
這樣至關於每次進行put 的時候都會進行同步當10個線程同步進行操做的時候,就會發現當第一個線程進去其餘線程必須等待第一個線程執行完成,才能夠進行下去。性能特別差。
分段鎖技術:ConcurrentHashMap相比 HashTable而言解決的問題就是的它不是鎖所有數據,而是鎖一部分數據,這樣多個線程訪問的時候就不會出現競爭關係。不須要排隊等待了。
從圖中能夠看出來ConcurrentHashMap的主幹是個Segment數組。、
它把區間按照併發級別(concurrentLevel),分紅了若干個segment。默認狀況下內部按併發級別爲16來建立。對於每一個segment的容量,默認狀況也是16。
ConcurrentHashMap是由Segment數組和HashEntry數組組成.
Segment是一種可重入鎖,在ConcurrentHashMap裏扮演鎖的角色;
HashEntry則用於存儲鍵值對數據.
一個ConcurrentHashMap裏包含一個Segment數組.
Segment的結構和HashMap相似,是一種數組和鏈表結構.
一個Segment裏包含一個HashEntry數組,每一個HashEntry是一個鏈表結構的元素,每一個Segment守護着一個HashEntry數組裏的元素,當對HashEntry數組的數據進行修改時,
必須首先得到與它對應的Segment鎖
這就是爲何ConcurrentHashMap支持容許多個修改同時併發進行,緣由就是採用的Segment分段鎖功能,每個Segment 都想的於小的hash table而且都有本身鎖,只要修改再也不同一個段上就不會引發併發問題。
雖然三個集合類在多線程併發應用中都是線程安全的,可是他們有一個重大的差異,就是他們各自實現線程安全的方式。