HashMap Java 8实现


92

按照以下链接文档:Java HashMap实现

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

首先

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

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

其次

如果您HashMap在JDK中看到的源代码,则将找到以下静态内部类:

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;
        }
    }
    //...
}

如何使用?我只想对算法进行解释

Answers:


225

HashMap包含一定数量的水桶。它用于hashCode确定将它们放入哪个存储桶。为了简单起见,将其想象为模数。

如果我们的哈希码为123456,并且我们有4个存储桶,123456 % 4 = 0那么该项目将进入第一个存储桶,即存储桶1。

哈希图

如果我们的哈希码功能很好,它应该提供均匀的分布,因此所有存储桶都将得到同等的使用。在这种情况下,存储桶使用链表存储值。

链桶

但是您不能依靠人们来实现良好的哈希函数。人们通常会编写较差的哈希函数,这将导致分布不均。我们也很可能对我们的输入感到不走运。

哈希表错误

这种分布越少,我们离O(1)运算的距离就越远,而我们朝O(n)运算的距离越近。

Hashmap的实现通过将一些存储桶组织到树中而不是链接列表(如果存储桶变得太大)来减轻这种情况。这TREEIFY_THRESHOLD = 8是为了什么。如果存储桶包含八个以上的项目,则它应成为一棵树。

树桶

这棵树是红黑树。它首先按哈希码排序。如果散列码相同,它使用compareTo的方法,Comparable如果对象实现该接口,否则身份哈希码。

如果从映射中删除了条目,则存储桶中的条目数可能会减少,从而不再需要此树结构。那就是UNTREEIFY_THRESHOLD = 6是为了。如果存储桶中的元素数降至六个以下,我们不妨回到使用链表的方式。

最后是MIN_TREEIFY_CAPACITY = 64

当哈希图的大小增加时,它会自动调整自身大小以具有更多存储桶。如果我们有一个小的哈希图,那么我们变得非常满的存储桶的可能性就很高,因为我们没有太多不同的存储桶可以放入东西。拥有更大的哈希映射,而更多的存储桶却没有那么满,那就更好了。该常量基本上表示如果我们的哈希图很小,则不要开始将桶变成树-应该首先将其调整为更大。


为了回答有关性能提升的问题,添加了这些优化以改善最坏的情况。我只是推测,但是如果您的hashCode功能不是很好,由于这些优化,您可能只会看到明显的性能改进。


3
不均匀分布并不总是表明散列函数不佳。例如String,某些数据类型的值空间int比哈希码大得多,因此,冲突是不可避免的。现在,它取决于String您放入映射中的实际值(例如real),而不论是否获得均匀分布。不好的分布可能是不幸的结果。
Holger

3
+1,我想补充一点,这种树方法缓解的特定情况是哈希冲突DOS攻击java.lang.String具有确定性的非加密特性hashCode,因此攻击者可以使用冲突的hashCode轻松创建不同的String。在此优化之前,这可能会将HashMap操作降级为O(n)时间,现在只是将其降级为O(log(n))。
MikeFHay

1
+1,if the objects implement that interface, else the identity hash code.我正在寻找其他部分。
Number945 '18

1
@NateGlenn如果您不覆盖默认的哈希码
迈克尔

我没有得到“如果我们的哈希图很小,则此常数基本上表示不要开始将桶变成树-应该首先将其调整为更大。” 为MIN_TREEIFY_CAPACITY。这是否意味着“一旦我们插入要散列到已经包含8(TREEIFY_THRESHOLD)个密钥的存储桶中的密钥,并且如果其中已经有64(MIN_TREEIFY_CAPACITY)个密钥HashMap,则该存储桶的链接列表将转换为平衡树。”
8:54

16

简而言之(尽可能简化)+一些更多细节。

这些属性取决于很多内部事物,在直接使用它们之前,这些事物非常容易理解。

TREEIFY_THRESHOLD- >当单个存储桶达到此值(并且总数超过MIN_TREEIFY_CAPACITY)时,它将转换为完美平衡的红色/黑色树节点。为什么?由于搜索速度快。用另一种方式思考:

这将需要最多32个步骤,以搜索与桶/箱中的条目,Integer.MAX_VALUE的条目。

下一个主题的一些介绍。为什么垃圾箱/桶的数量始终是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被“开玩笑”为:fast,fast,fast,slooow。还有其他实现-搜索无休止的哈希图 ...

现在,UNTREEIFY_THRESHOLD在重新哈希后起作用。到那时,某些条目可能会从此bin移到其他bin(它们在(n-1)&hash计算中又增加了一位,因此可能移至其他存储桶),并且可能达到了UNTREEIFY_THRESHOLD。在这一点上,将bin保持为并不能带来回报red-black tree nodeLinkedList相反,像

 entry.next.next....

MIN_TREEIFY_CAPACITY是将特定存储桶转换为树之前的最小存储桶数。


10

TreeNode是存储属于单个仓的条目的另一种方法HashMap。在较旧的实现中,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.

完全正确。如果它们通过TREEIFY_THRESHOLD 并且仓的总数至少为MIN_TREEIFY_CAPACITY。我已经在回答中尝试了这一点……
尤金(Eugene)

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中,所有键都是此类的实例。例如

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的增长,差异变得更加明显。


它无法构建高效的树,因为除了散列码(均相同)和equals方法(均无济于事)之外,它无法比较键。
user253751

@immibis它们的哈希码不一定相同。它们很有可能不同。如果这些类实现了它,它将另外使用compareTofrom ComparableidentityHashCode是它使用的另一种机制。
迈克尔”

@Michael在此示例中,所有哈希码必须相同,并且该类未实现Comparable。identityHashCode将无法找到正确的节点。
user253751

@immibis啊,是的,我只是略读了一下,但是你是对的。因此,由于Key未实现ComparableidentityHashCode将使用它:)
迈克尔(Michael)

@EmonMishra不幸的是,简单地以视觉是不够的,我试图以盖在我的答案。
尤金(Eugene)

2

HashMap实现的更改是在JEP-180中添加的。目的是:

通过使用平衡树而不是链接列表来存储地图条目,在高哈希冲突条件下提高java.util.HashMap的性能。在LinkedHashMap类中实现相同的改进

但是,纯粹的性能并不是唯一的收益。万一使用哈希映射表存储用户输入,它还可以防止 HashDoS攻击,因为用于在存储桶中存储数据的红黑树在O(log n)中具有最坏情况的插入复杂度。在满足一定条件后才使用该树-请参见Eugene的答案


-1

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

真正的哈希函数必须遵循此规则–

“哈希函数每次将函数应用于相同或相等的对象时,都应返回相同的哈希码。换句话说,两个相等的对象必须一致地产生相同的哈希码。”


这不能回答问题。
Stephen C
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.