[英]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.