[英]HashMap Java 8 implementation
根據以下鏈接文檔: Java HashMap 實現
我對HashMap
的實現(或者更確切地說,是HashMap
的增強)感到困惑。 我的查詢是:
首先
static final int TREEIFY_THRESHOLD = 8;
static final int UNTREEIFY_THRESHOLD = 6;
static final int MIN_TREEIFY_CAPACITY = 64;
為什么以及如何使用這些常量? 我想要一些明確的例子。 他們如何通過此實現性能提升?
第二
如果在JDK中查看HashMap
的源碼,會發現如下靜態內部類:
static final class TreeNode<K, V> extends java.util.LinkedHashMap.Entry<K, V> {
HashMap.TreeNode<K, V> parent;
HashMap.TreeNode<K, V> left;
HashMap.TreeNode<K, V> right;
HashMap.TreeNode<K, V> prev;
boolean red;
TreeNode(int arg0, K arg1, V arg2, HashMap.Node<K, V> arg3) {
super(arg0, arg1, arg2, arg3);
}
final HashMap.TreeNode<K, V> root() {
HashMap.TreeNode arg0 = this;
while (true) {
HashMap.TreeNode arg1 = arg0.parent;
if (arg0.parent == null) {
return arg0;
}
arg0 = arg1;
}
}
//...
}
它是如何使用的? 我只想解釋一下算法。
HashMap
包含一定數量的桶。 它使用hashCode
來確定將這些放入哪個桶。 為簡單起見,把它想象成一個模數。
如果我們的哈希碼是 123456 並且我們有 4 個桶,那么123456 % 4 = 0
那么該項目進入第一個桶,即桶 1。
如果我們的hashCode
函數是好的,它應該提供一個均勻的分布,這樣所有的桶都會被平均使用。 在這種情況下,存儲桶使用鏈表來存儲值。
但是你不能依靠人來實現好的哈希函數。 人們經常會編寫糟糕的散列函數,這將導致分布不均勻。 我們也有可能只是因為我們的輸入而倒霉。
這種分布越不均勻,我們離 O(1) 操作越遠,離 O(n) 操作越近。
如果桶變得太大,HashMap 的實現試圖通過將一些桶組織成樹而不是鏈表來緩解這種情況。 這就是TREEIFY_THRESHOLD = 8
的用途。 如果一個桶包含八個以上的項目,它應該成為一棵樹。
這棵樹是一棵紅黑樹,大概是因為它提供了一些最壞情況的保證。 它首先按哈希碼排序。 如果散列碼相同,它使用compareTo
的方法Comparable
如果對象實現該接口,否則身份哈希碼。
如果從映射中刪除條目,則存儲桶中的條目數量可能會減少,從而不再需要此樹結構。 這就是UNTREEIFY_THRESHOLD = 6
的用途。 如果桶中的元素數量低於 6,我們不妨回到使用鏈表。
最后,還有MIN_TREEIFY_CAPACITY = 64
。
當哈希映射的大小增加時,它會自動調整自身大小以擁有更多存儲桶。 如果我們有一個小的 HashMap,我們得到非常滿的桶的可能性非常高,因為我們沒有那么多不同的桶可以放入東西。 擁有更大的 HashMap 更好,更多的桶不那么滿。 這個常量基本上是說如果我們的 HashMap 非常小,不要開始將桶變成樹 - 它應該首先調整大小以變大。
為了回答您關於性能提升的問題,添加了這些優化以改善最壞的情況。 如果您的hashCode
函數不是很好,您可能只會看到由於這些優化而顯着的性能改進。
它旨在防止不良的hashCode
實現,並提供基本的碰撞攻擊保護,在碰撞攻擊中,不良行為者可能會通過故意選擇占用相同存儲桶的輸入來嘗試減慢系統速度。
說得更簡單(盡可能簡單)+更多細節。
這些屬性取決於很多內部的東西,在直接移動到它們之前,這些東西很容易理解。
TREEIFY_THRESHOLD -> 當單個桶達到這個(並且總數超過MIN_TREEIFY_CAPACITY
)時,它會轉化為一個完美平衡的紅/黑樹節點。 為什么? 因為搜索速度。 換個角度想一想:
最多需要 32 個步驟來搜索具有Integer.MAX_VALUE條目的存儲桶/bin 中的條目。
下一個主題的一些介紹。 為什么 bins/buckets 的數量總是 2 的冪? 至少有兩個原因:比取模運算快和對負數取模將是負數。 並且您不能將 Entry 放入“否定”存儲桶中:
int arrayIndex = hashCode % buckets; // will be negative
buckets[arrayIndex] = Entry; // obviously will fail
相反,使用了一個很好的技巧來代替模數:
(n - 1) & hash // n is the number of bins, hash - is the hash function of the key
這在語義上與模運算相同。 它將保留較低的位。 當你這樣做時,這會產生一個有趣的結果:
Map<String, String> map = new HashMap<>();
在上述情況下,僅根據哈希碼的最后 4 位來決定條目的去向。
這就是將桶相乘的地方。 在某些情況下(需要花很多時間來詳細解釋),桶的大小會翻倍。 為什么? 當桶的大小增加一倍時,還有一個位開始發揮作用。
所以你有 16 個桶——哈希碼的最后 4 位決定了條目的去向。 您將存儲桶加倍:32 個存儲桶 - 最后 5 位決定條目的去向。
因此,此過程稱為重新散列。 這可能會變慢。 也就是說(對於關心的人)因為 HashMap 被“開玩笑”為: fast, fast, fast, slooow 。 還有其他實現 - 搜索無暫停哈希圖......
現在UNTREEIFY_THRESHOLD在重新散列后開始發揮作用。 在這一點上,一些條目可能會從這個 bin 移動到其他 bin(它們在(n-1)&hash
計算中再添加一位 - 因此可能會移動到其他bucket)並且它可能會到達這個UNTREEIFY_THRESHOLD
。 在這一點上,將 bin 保持為red-black tree node
並沒有回報,而是作為LinkedList
代替,例如
entry.next.next....
MIN_TREEIFY_CAPACITY是將某個桶轉換為樹之前的最小桶數。
TreeNode
是存儲屬於HashMap
單個 bin 的條目的另一種方法。 在較舊的實現中,bin 的條目存儲在鏈表中。 在 Java 8 中,如果 bin 中的條目數超過閾值 ( TREEIFY_THRESHOLD
),它們將存儲在樹結構中,而不是原始鏈表中。 這是一個優化。
從實施來看:
/*
* Implementation notes.
*
* This map usually acts as a binned (bucketed) hash table, but
* when bins get too large, they are transformed into bins of
* TreeNodes, each structured similarly to those in
* java.util.TreeMap. Most methods try to use normal bins, but
* relay to TreeNode methods when applicable (simply by checking
* instanceof a node). Bins of TreeNodes may be traversed and
* used like any others, but additionally support faster lookup
* when overpopulated. However, since the vast majority of bins in
* normal use are not overpopulated, checking for existence of
* tree bins may be delayed in the course of table methods.
您需要對其進行可視化:假設有一個類鍵,只有 hashCode() 函數被覆蓋以始終返回相同的值
public class Key implements Comparable<Key>{
private String name;
public Key (String name){
this.name = name;
}
@Override
public int hashCode(){
return 1;
}
public String keyName(){
return this.name;
}
public int compareTo(Key key){
//returns a +ve or -ve integer
}
}
然后在其他地方,我將 9 個條目插入到 HashMap 中,所有鍵都是此類的實例。 例如
Map<Key, String> map = new HashMap<>();
Key key1 = new Key("key1");
map.put(key1, "one");
Key key2 = new Key("key2");
map.put(key2, "two");
Key key3 = new Key("key3");
map.put(key3, "three");
Key key4 = new Key("key4");
map.put(key4, "four");
Key key5 = new Key("key5");
map.put(key5, "five");
Key key6 = new Key("key6");
map.put(key6, "six");
Key key7 = new Key("key7");
map.put(key7, "seven");
Key key8 = new Key("key8");
map.put(key8, "eight");
//Since hascode is same, all entries will land into same bucket, lets call it bucket 1. upto here all entries in bucket 1 will be arranged in LinkedList structure e.g. key1 -> key2-> key3 -> ...so on. but when I insert one more entry
Key key9 = new Key("key9");
map.put(key9, "nine");
threshold value of 8 will be reached and it will rearrange bucket1 entires into Tree (red-black) structure, replacing old linked list. e.g.
key1
/ \
key2 key3
/ \ / \
樹遍歷 {O(log n)} 比 LinkedList {O(n)} 快,並且隨着 n 的增長,差異變得更加顯着。
JEP-180添加了對 HashMap 實現的更改。 目的是:
通過使用平衡樹而不是鏈表來存儲映射條目,提高了 java.util.HashMap 在高哈希沖突條件下的性能。 在 LinkedHashMap 類中實現相同的改進
然而,純粹的性能並不是唯一的收獲。 它還可以防止HashDoS 攻擊,以防使用哈希映射來存儲用戶輸入,因為用於存儲桶中數據的紅黑樹具有 O(log n) 的最壞情況插入復雜度。 在滿足特定條件后使用該樹 - 請參閱Eugene 的回答。
要了解 hashmap 的內部實現,您需要了解散列。 散列以其最簡單的形式,是一種在將任何公式/算法應用於其屬性后為任何變量/對象分配唯一代碼的方法。
一個真正的哈希函數必須遵循這個規則——
“當函數應用於相同或相等的對象時,哈希函數每次都應該返回相同的哈希碼。 換句話說,兩個相等的對象必須始終如一地產生相同的哈希碼。”
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.