根据以下链接文档:Java HashMap Implementation
我对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;
}
}
//...
}
如何使用? 我只想要解释算法。
答案 0 :(得分:199)
HashMap
包含一定数量的存储桶。它使用hashCode
来确定将这些存储桶放入哪个存储桶。为简单起见,将其想象为模数。
如果我们的哈希码是123456并且我们有4个桶,123456 % 4 = 0
那么该项目就在第一个桶中,即Bucket 1。
如果我们的哈希码函数很好,它应该提供均匀分布,所以所有的桶都会在某种程度上同等使用。在这种情况下,存储桶使用链接列表来存储值。
但是你不能依赖人们来实现好的哈希函数。人们经常会写出糟糕的哈希函数,这会导致非均匀分布。我们也可能因为输入而感到不幸。
这种分布越不均匀,我们越是从O(1)运算开始,越接近O(n)运算。
Hashmap的实现尝试通过在存储桶变得太大的情况下将一些存储桶组织到树而不是链接列表来缓解这种情况。这是TREEIFY_THRESHOLD = 8
的用途。如果一个桶包含八个以上的项目,它应该变成一棵树。
这棵树是红黑树。它首先按哈希码排序。如果哈希码相同,则如果对象实现该接口,则使用compareTo
Comparable
方法,否则使用标识哈希码。
如果从地图中删除条目,则存储桶中的条目数可能会减少,从而不再需要此树结构。这就是UNTREEIFY_THRESHOLD = 6
的用途。如果存储桶中的元素数量低于6,我们也可以回到使用链表。
最后,有MIN_TREEIFY_CAPACITY = 64
。
当哈希映射的大小增加时,它会自动调整自身大小以获得更多存储桶。如果我们有一个小的哈希映射,我们获得非常完整的桶的可能性是相当高的,因为我们没有很多不同的桶来放入内容。拥有更大的哈希映射会更好,更多的桶不够饱满。如果我们的哈希映射非常小,这个常量基本上不会开始将桶变成树 - 它应该首先调整大小。
要回答有关性能提升的问题,我们会添加这些优化以改善最差情况。我只是在推测,但如果你的hashCode
功能不是很好,你可能只会看到明显的性能提升。
图像是我的(感谢MSPaint)。无论如何重复使用它们。
答案 1 :(得分:12)
更简单(尽可能简单)+更多细节。
这些属性取决于许多内部事物,这些内容很难理解 - 直到他们直接转移到它们之前。
TREEIFY_THRESHOLD - &gt;当单个存储桶达到此值(并且总数超过MIN_TREEIFY_CAPACITY
)时,它将转换为完美平衡的红/黑树节点。为什么?因为搜索速度快。以不同的方式考虑它:
最多32步,需要使用 Integer.MAX_VALUE 条目在存储桶/ bin中搜索条目。
下一主题的一些介绍。 为什么垃圾桶/桶的数量总是2的幂?至少有两个原因:比模运算更快,负数的模数将为负。你不能把一个条目放入“负面”桶中:
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被“开玩笑”为:快速,快速,快速,低潮。还有其他实现 - 搜索 pauseless hashmap ...
现在 UNTREEIFY_THRESHOLD 在重新散列后开始播放。此时,某些条目可能会从此二进制文件移动到其他条目(它们会向(n-1)&hash
计算添加一个位 - 因此可能会转移到其他存储区)并且可能会达到此值{ {1}}。此时,将广告资源保留为UNTREEIFY_THRESHOLD
并将其作为red-black tree node
而不是
LinkedList
MIN_TREEIFY_CAPACITY 是将某个存储桶转换为树之前的最小存储桶数。
答案 2 :(得分:8)
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.
答案 3 :(得分:3)
您需要对其进行可视化:假设有一个Class Key,只覆盖了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中,所有键都是此类的实例。 e.g。
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
/ \ / \
树遍历比LinkedList {O(n)}更快{O(log n)},随着n的增长,差异变得更加显着。
答案 4 :(得分:2)
HashMap实现的更改已添加JEP-180。目的是:
通过使用平衡树而不是链接列表来存储映射条目,在高哈希冲突条件下提高java.util.HashMap的性能。在LinkedHashMap类中实现相同的改进
然而,纯粹的表现不是唯一的收获。如果使用哈希映射来存储用户输入,它也会阻止 HashDoS attack,因为用于在存储桶中存储数据的red-black tree具有最差的插入复杂度在O(log n)。在满足特定条件后使用树 - 请参阅Eugene's answer。
答案 5 :(得分:0)
要了解哈希图的内部实现,您需要了解哈希。 以最简单的形式进行哈希处理是一种在对属性应用任何公式/算法之后为任何变量/对象分配唯一代码的方法。
真正的哈希函数必须遵循此规则–
“哈希函数每次将函数应用于相同或相等的对象时,都应返回相同的哈希码。换句话说,两个相等的对象必须一致地产生相同的哈希码。”