Java HashSet(雜湊集),HashMap(雜湊對映)的簡單介紹
阿新 • • 發佈:2021-01-05
簡介
本篇將簡單講解Java集合框架中的HashSet與HashMap。
雜湊集(HashSet)
快速入門
- 底層原理:動態陣列加單向連結串列或紅黑樹。JDK 1.8之後,當連結串列長度超過閾值8時,連結串列將轉換為紅黑樹。
- 查閱HashSet的原始碼,可以看到HashSet的底層是HashMap,HashSet相當於只用了HashMap鍵Key的部分,當需要進行新增元素操作時,其值Value始終為常量PRESENT = new Object()。以下為HashSet的程式碼片段:
private transient HashMap<E,Object> map; public HashSet() { map = new HashMap<>(); } public boolean add(E e) { return map.put(e,PRESENT)==null; } public Iterator<E> iterator() { return map.keySet().iterator(); }
- 上面說到,在JDK 1.8之後,當連結串列長度超過閾值8時,連結串列將轉為紅黑樹;當連結串列長度小於6時,紅黑樹重新轉為連結串列。那麼為什麼閾值是8呢?
- 閾值定義為8,符合數學概率論上的泊松分佈Poisson。根據泊松分佈,一個桶bucket是很難被填滿達到長度8的。
- 一旦用於儲存資料的連結串列長度達到閾值8,則很大的可能是該HashSet所使用的雜湊函式效能不佳、或存在惡意程式碼向集中添加了很多具有相同雜湊碼的值,此時轉為平衡二叉樹可以提高效能。
散列表
- 連結串列LinkedList、陣列Array或陣列列表ArrayList都有一個共同的缺點:根據值查詢元素速度慢。一旦存放的資料較多,查詢速度將十分緩慢。
- 如果應用中開發者不在意元素的排列順序,此時推薦使用的資料結構為散列表。散列表用於快速查詢物件。
- 使用散列表的關鍵是物件必須具備一個雜湊碼,通過物件內HashCode()方法即可計算得到物件的雜湊碼。一般情況下,不同資料的物件將產生不同的雜湊碼。
- 下表顯示了使用String類中hashCode()方法成的雜湊碼:
字串 | 雜湊碼 |
"Lee" | 76268 |
"lee" | 107020 |
"eel" | 100300 |
- 在Java中,散列表HashTable使用動態陣列加連結串列或紅黑樹的形式實現。
- 動態陣列中的每個位置被稱為桶bucket。要想查詢元素位於散列表中的位置,需要首先計算元素的雜湊碼,然後與桶的總數取餘,所得到的結果就是儲存這個元素的桶的索引。
- 假設動態陣列為table,物件a的雜湊碼為hashCode,則元素將存放在table的索引為hashCode % table.size(),通常將該索引值成為雜湊值,它與雜湊碼是不一樣的。
- 例如,如果某個物件的雜湊碼為76268,並且有128個桶,那麼這個物件應該儲存在第108號桶中,因為76268%128=108。
- 如果在這個桶中沒有其他的元素,此時將元素直接插入到桶中即可;但如果桶已經被填充,這種現象被稱為雜湊衝突hash collision。發生雜湊衝突,需要將新物件與桶中的所有物件進行比較,檢視這個物件是否已經存在。
- 此時如果雜湊碼合理地隨機分佈(可以理解為雜湊函式hashCode()合理),桶的數目也足夠大,需要比較的次數就會很少。
- 在Java 8中,桶滿時會從連結串列變為平衡二叉樹。如果選擇的雜湊函式不好,會產生很多衝突,或者如果有惡意程式碼試圖在散列表中填充多個有相同雜湊碼的值,這樣改為平衡二叉樹能提高效能。
- 如果需要更多地控制散列表的效能,可以指定一個初始的桶數。桶數是指用於收集具有相同雜湊值的桶的數目。如果要插入到散列表中的元素太多,就會增加衝突數量,降低檢索的效能。
- 如果大致知道最終會有多少個元素要插入到散列表中,就可以設定桶數。通常,將桶數設定為預計元素個數的75%~150%。有些研究人員認為:最好將桶數設定為一個素數,以防止鍵的聚集。不過,對此並沒有確鑿的證據。
- 標準類庫使用的桶數是2的次冪,預設值為16(為表大小提供的任何值,都將自動轉換為2的下一個冪值)。
- 但是,並不總能夠知道需要儲存多少個元素,也有可能最初的估計過低。如果散列表太滿,就需要再雜湊rehashed。如果要對散列表再雜湊,就需要建立一個桶數更多的表,並將所有元素插入到這個新表中,然後丟棄原來的表。裝填因子load factor可以確定何時對散列表進行再雜湊。
- 例如,如果裝填因子是0.75(預設值),說明表中已經填滿了75%以上,就會自動再雜湊,新表的桶數是原來的兩倍。對於大多數程式來說,裝填因子為0.75是合理的。
- 散列表可以用於實現很多重要的資料結構,其中最簡單的是集型別。集是沒有重複元素的元素集合,其中add方法首先會在這個集中查詢要新增的物件,如果不存在,就新增這個物件。
- Java集合框架提供了一個HashSet類,它實現了基於散列表的集。可以用add方法新增元素。contains方法已經被重新定義,用來快速查詢某個元素是否已經在集中。它只檢視一個桶中的元素,而不必檢視集合中所有元素。
- 雜湊集迭代器將依次訪問所有的桶,由於雜湊將元素分散在表中,所以會以一種看起來隨機的順序訪問元素。只有不關心集合中元素的順序時,才應該使用HashSet。
- 而HashSet的實現基於HashMap,在隨後會對HashMap的部分原始碼進行分析,以瞭解其初始容量及擴容機制。
雜湊對映(HashMap)
快速入門
- 底層原理:動態陣列加單向連結串列或紅黑樹。JDK 1.8之後,當連結串列長度超過閾值8時,連結串列將轉換為紅黑樹。預設散列表中的動態陣列長度為16,雜湊因子為0.75,擴容閾值為12。
- 擴容機制:擴容後散列表中的動態陣列長度,變為舊動態陣列的兩倍。擴容閾值為雜湊因子與動態陣列長度的乘積。
- 以下為HashMap中代表單向連結串列結構的Node<K,V>類,與代表紅黑樹結構的TreeNode<K,V>類。
// HashMap.java原始碼 // 基於單向連結串列的用於儲存資料的物件 static class Node<K,V> implements Map.Entry<K,V> { final int hash; final K key; V value; Node<K,V> next; Node(int hash,K key,V value,Node<K,V> next) { this.hash = hash; this.key = key; this.value = value; this.next = next; } ... } // 基於紅黑樹的用於儲存資料的物件 static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> { TreeNode<K,V> parent; // red-black tree links TreeNode<K,V> left; TreeNode<K,V> right; TreeNode<K,V> prev; // needed to unlink next upon deletion boolean red; TreeNode(int hash,V val,V> next) { super(hash,key,val,next); } ... }
二次雜湊
雜湊對映HashMap只對鍵進行雜湊,與鍵關聯的值不進行雜湊。以下為HashMap中的部分原始碼:
public V put(K key,V value) { return putVal(hash(key),value,false,true); } static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }
- 所有使用put()方法存入HashMap中的鍵值對,都會在內部呼叫putVal()進行新增元素操作。putVal()方法的第一個引數則需要提供key的雜湊碼。
- 此處並沒有直接使用key.hashCode(),而是使用了HashMap中的hash()方法對key進行二次雜湊。二次雜湊可以理解為在物件呼叫它的雜湊函式之後,再進行一次額外的計算。二次雜湊有助於獲得更好的雜湊碼。
擴容機制
- HashMap中的動態陣列初始容量為16,預設的雜湊因子為0.75,即在容量到達16 * 0.75 = 12時,會對動態陣列進行擴容處理,上限容量被稱為threshold。
- 擴容後的HashMap,其動態陣列容量為原來的2倍,由於雜湊因子不會改變,因此threshold也為原來的2倍。
- 以下為HashMap中resize()、putVal()的原始碼:
final Node<K,V>[] resize() { Node<K,V>[] oldTab = table; int oldCap = (oldTab == null) ? 0 : oldTab.length; int oldThr = threshold; int newCap,newThr = 0; if (oldCap > 0) { if (oldCap >= MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return oldTab; } else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) newThr = oldThr << 1; // double threshold } else if (oldThr > 0) // initial capacity was placed in threshold newCap = oldThr; else { // zero initial threshold signifies using defaults newCap = DEFAULT_INITIAL_CAPACITY; newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); } if (newThr == 0) { float ft = (float)newCap * loadFactor; newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE); } threshold = newThr; @SuppressWarnings({"rawtypes","unchecked"}) Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; table = newTab; if (oldTab != null) { for (int j = 0; j < oldCap; ++j) { Node<K,V> e; if ((e = oldTab[j]) != null) { oldTab[j] = null; if (e.next == null) newTab[e.hash & (newCap - 1)] = e; else if (e instanceof TreeNode) ((TreeNode<K,V>)e).split(this,newTab,j,oldCap); else { // preserve order Node<K,V> loHead = null,loTail = null; Node<K,V> hiHead = null,hiTail = null; Node<K,V> next; do { next = e.next; if ((e.hash & oldCap) == 0) { if (loTail == null) loHead = e; else loTail.next = e; loTail = e; } else { if (hiTail == null) hiHead = e; else hiTail.next = e; hiTail = e; } } while ((e = next) != null); if (loTail != null) { loTail.next = null; newTab[j] = loHead; } if (hiTail != null) { hiTail.next = null; newTab[j + oldCap] = hiHead; } } } } } return newTab; } final V putVal(int hash,boolean onlyIfAbsent,boolean evict) { Node<K,V>[] tab; Node<K,V> p; int n,i; if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; // 第一個resize()是進行動態陣列Node<K,V>[]初始化的操作,不會進行擴容 if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash,null); else { Node<K,V> e; K k; if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p; else if (p instanceof TreeNode) e = ((TreeNode<K,V>)p).putTreeVal(this,tab,hash,value); else { for (int binCount = 0; ; ++binCount) { if ((e = p.next) == null) { p.next = newNode(hash,null); if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st treeifyBin(tab,hash); break; } if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; } } if (e != null) { // existing mapping for key V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); return oldValue; } } ++modCount; // 當HashMap中元素數量大於閾值threshold,則會進行擴容resize()操作 if (++size > threshold) resize(); afterNodeInsertion(evict); return null; }
- 通過原始碼可以知道,HashMap在初始化的時候並不會立即為動態陣列分配記憶體,直到呼叫putVal()為止,才會在putVal()中呼叫resize()方法初始化動態陣列。
- 動態陣列Node<K,V>[]將在resize()中完成初始化或擴容的操作。
- 其中有關初始化的關鍵程式碼為:
newCap = DEFAULT_INITIAL_CAPACITY; // DEFAULT_INITIAL_CAPACITY = 1 << 4,即預設大小為16。 newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); // threshold = newCap * 0.75,即預設為12。
- 有關於擴容的關鍵程式碼為:
if (oldCap > 0) { // 當動態陣列擁有預設容量時,如果再次呼叫resize(),則一定會進行擴容操作 if (oldCap >= MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return oldTab; } else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) { // 容量為原來的2倍 newThr = oldThr << 1; // 閾值為原來的2倍 } }
總結
以上為所有關於HashSet、HashMap的粗略介紹。
如果希望瞭解更多的內容,可以前往JDK閱讀原始碼。
以上就是Java HashSet(雜湊集),HashMap(雜湊對映)的簡單介紹的詳細內容,更多關於Java HashSet和HashMap的資料請關注我們其它相關文章!