HashMapJava8实现

根据以下链接文档: Java HashMap 实现

我对 HashMap的实现(或者更确切地说,对 HashMap的增强)感到困惑:

首先

static final int TREEIFY_THRESHOLD = 8;
static final int UNTREEIFY_THRESHOLD = 6;
static final int MIN_TREEIFY_CAPACITY = 64;

为什么以及如何使用这些常量? < strong > 我想要一些明确的例子。 他们是如何通过这种方式获得性能提升的?

其次

如果你在 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;
}
}
//...
}

如何使用。

35459 次浏览

TreeNode是另一种存储属于 HashMap的单个 bin 的条目的方法。在较早的实现中,bin 的条目存储在一个链表中。在 Java8中,如果 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.

HashMap包含一定数量的桶。它使用 hashCode来确定将这些文件放入哪个桶中。为了简单起见,把它想象成一个模。

If our hashcode is 123456 and we have 4 buckets, 123456 % 4 = 0 so the item goes in the first bucket, Bucket 1.

HashMap

如果我们的 hashCode函数是好的,它应该提供一个均匀的分布,以便所有的桶将被使用有些平等。在这种情况下,bucket 使用链表来存储值。

Linked Buckets

But you can't rely on people to implement good hash functions. People will often write poor hash functions which will result in a non-even distribution. It's also possible that we could just get unlucky with our inputs.

Bad hashmap

这个分布越不均匀,我们离 O (1)操作越远,离 O (n)操作越近。

HashMap 的实现试图通过将一些桶组织到树中而不是在桶变得过大时使用链表来缓解这种情况。这就是 TREEIFY_THRESHOLD = 8的作用。如果一个桶包含八个以上的项目,它应该成为一棵树。

Tree Bucket

这棵树是 红黑树,选择它可能是因为它提供了一些最坏情况的保证。它首先按哈希代码排序。如果哈希代码相同,则使用 ComparablecompareTo方法,如果对象实现该接口,则使用标识哈希代码。

If entries are removed from the map, the number of entries in the bucket might reduce such that this tree structure is no longer necessary. That's what the UNTREEIFY_THRESHOLD = 6 is for. If the number of elements in a bucket drops below six, we might as well go back to using a linked list.

Finally, there is the MIN_TREEIFY_CAPACITY = 64.

当散列映射的大小增长时,它会自动调整自己的大小以便拥有更多的存储桶。如果我们有一个小的 HashMap,我们得到非常满的桶的可能性是相当高的,因为我们没有那么多不同的桶来放东西。最好有一个更大的 HashMap,带有更多不那么满的桶。这个常量基本上是说,如果我们的 HashMap 非常小,就不要开始将桶制作成树——它应该首先调整大小,使其更大。


To answer your question about the performance gain, these optimisations were added to improve the worst case. You would probably only see a noticeable performance improvement because of these optimisations if your hashCode function was not very good.

它旨在防止坏的 hashCode实现,并提供基本的 碰撞攻击保护,在这种情况下,坏的参与者可能通过故意选择占用相同桶的输入来试图减慢系统的速度。

您需要对它进行可视化: 假设有一个 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
}


}

然后在其他地方,我在 HashMap 中插入9个条目,所有的键都是这个类的实例。例如:。

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 (logn)} ,随着 n 的增长,差异变得更加显著。

更简单的说(尽可能的简单) + 更多的细节。

这些属性依赖于很多内部的东西,这些东西在直接转移到它们之前非常容易理解。

TREEIFY_THRESHOLD -> when a 单身 bucket reaches this (and the total number exceeds MIN_TREEIFY_CAPACITY), it is transformed into a 完美平衡的红/黑树节点. Why? Because of search speed. Think about it in a different way:

它将使用 最多32步在带有 Integer.MAX_VALUE条目的 bucket/bin 中搜索条目。

Some intro for the next topic. 为什么垃圾桶的数量总是2的幂次方? At least two reasons: faster than modulo operation and modulo on negative numbers will be negative. And you can't put an Entry into a "negative" bucket:

 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

这就是 ABc0的模除,它会保留较低的比特,当你这样做的时候,会得到一个有趣的结果:

Map<String, String> map = new HashMap<>();

在上面的例子中,决定一个条目的去向是基于您的 hashcode 的 on the last 4 bits only

这时候就需要乘以桶了。在某些条件下(在 确切的细节中需要花费大量时间来解释) ,桶的大小增加了一倍。为什么?当桶的大小增加一倍时,就会有另一个位进场.

所以你有16个桶-散列码的最后4位决定了一个条目的去向。你双倍的桶: 32桶-最后5位决定进入将去哪里。

As such this process is called re-hashing. This might get slow. That is (for people who care) as HashMap is "joked" as: 快,快,快,慢. There are other implementations - search 暂停散列表...

现在 门槛在重新散列之后开始运行。这时,一些条目可能会从这个存储桶移动到其他存储桶(它们会给 (n-1)&hash计算增加一个位——因此可能会移动到 其他存储桶) ,它可能会到达这个 UNTREEIFY_THRESHOLD。在这一点上,它不支付保持垃圾桶作为 red-black tree node,但作为一个 LinkedList代替,如

 entry.next.next....

MIN _ TREEIFY _ CAPACITY 是在某个桶被转换成树之前的最小桶数。

JEP-180中增加了对 HashMap 实现的更改,目的是:

改进 java.util 的性能。HashMap 在高散列冲突条件下使用平衡树而不是链表来存储映射条目。在 LinkedHashMap 类中实现相同的改进

However pure performance is not the only gain. It will also 预防 HashDoS attack, in case a hash map is used to store user input, because the 红黑相间的树 that is used to store data in the bucket has worst case insertion complexity in O(log n). The tree is used after a certain criteria is met - see 尤金的回答.

要理解散列表的内部实现,您需要理解散列表。 散列最简单的形式是在对任何变量/对象的属性应用任何公式/算法之后,为其分配唯一代码的一种方法。

真正的哈希函数必须遵循以下规则-

”当函数应用于相同或相等的对象时,散列函数应每次返回相同的散列代码。换句话说,两个相等的对象必须一致地产生相同的散列码。”