HashMap Java 8 实现

Posted

技术标签:

【中文标题】HashMap Java 8 实现【英文标题】:HashMap Java 8 implementation 【发布时间】:2017-10-10 05:13:10 【问题描述】:

根据以下链接文档:Java HashMap Implementation

我对@9​​87654322@ 的实现感到困惑(或者更确切地说,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;
        
    
    //...

它是如何使用的? 我只想解释一下算法

【问题讨论】:

【参考方案1】:

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

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

如果我们的hashCode 函数很好,它应该提供一个均匀的分布,这样所有的桶都会被平等地使用。在这种情况下,存储桶使用链表来存储值。

但是你不能依靠人来实现好的散列函数。人们经常会编写糟糕的哈希函数,这将导致分布不均匀。也有可能我们的输入不走运。

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

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

这棵树是Red-Black tree,大概是因为它提供了一些最坏情况的保证。它首先按哈希码排序。如果哈希码相同,如果对象实现该接口,则使用ComparablecompareTo 方法,否则使用身份哈希码。

如果从映射中删除条目,则桶中的条目数可能会减少,从而不再需要此树结构。这就是UNTREEIFY_THRESHOLD = 6 的用途。如果桶中元素的数量低于 6 个,我们还不如回到使用链表。

最后是MIN_TREEIFY_CAPACITY = 64

当哈希映射的大小增加时,它会自动调整自己的大小以拥有更多的桶。如果我们有一个小的 HashMap,我们得到非常满的桶的可能性非常高,因为我们没有那么多不同的桶来放东西。拥有一个更大的 HashMap 会好得多,并且有更多的不那么满的桶。这个常数基本上表示如果我们的 HashMap 非常小,则不要开始将桶制作成树 - 它应该首先调整大小以使其变大。


为了回答您关于性能提升的问题,添加了这些优化以改善最坏的情况。如果您的 hashCode 功能不是很好,您可能只会看到这些优化带来的显着性能提升。

它旨在防止不良的hashCode 实现,还提供基本的collision attacks 保护,在这种情况下,不良行为者可能会通过故意选择占用相同存储桶的输入来降低系统速度。

【讨论】:

【参考方案2】:

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

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

TREEIFY_THRESHOLD ->当一个单个桶达到这个值(并且总数超过MIN_TREEIFY_CAPACITY),它被转换成一个完美平衡的红/黑树节点。为什么?因为搜索速度。换个角度想一想:

在具有 Integer.MAX_VALUE 个条目的存储桶/bin 中搜索条目需要最多 32 个步骤

下一个主题的一些介绍。 为什么箱子/桶的数量总是 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。还有其他实现 - 搜索 pauseless hashmap...

现在 UNTREEIFY_THRESHOLD 在重新散列后开始发挥作用。那时,一些条目可能会从这个 bin 移动到其他 bin(它们向 (n-1)&amp;hash 计算添加了一位 - 因此可能会移动到 other 存储桶)并且它可能会到达这个 UNTREEIFY_THRESHOLD .此时,将 bin 保留为 red-black tree node 并没有回报,而是将其保留为 LinkedList,例如

 entry.next.next....

MIN_TREEIFY_CAPACITY是在某个桶变成树之前的最小桶数。

【讨论】:

【参考方案3】:

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.

【讨论】:

完全正确。如果它们通过TREEIFY_THRESHOLD AND,则 bin 的总数至少为 MIN_TREEIFY_CAPACITY。我试图在我的回答中涵盖这一点......【参考方案4】:

您需要将其可视化:假设有一个类键,只有 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 的增长,差异变得更加显着。

【讨论】:

它不可能构建一个高效的树,因为它无法比较键,除了它们的哈希码,它们都是相同的,它们的等于方法,这对排序没有帮助。 @immibis 他们的哈希码不一定相同。他们很可能不同。如果类实现了它,它将额外使用来自ComparablecompareToidentityHashCode 是它使用的另一种机制。 @Michael 在此示例中,所有哈希码都必须相同,并且该类未实现 Comparable。 identityHashCode 在找到正确的节点时将毫无价值。 @immibis 啊,是的,我只是略读了一下,但你是对的。所以,由于Key 没有实现ComparableidentityHashCode 将被使用:) @EmonMishra 不幸的是,简单地 视觉是不够的,我试图在我的回答中涵盖这一点。【参考方案5】:

HashMap 实现的变化是用JEP-180 添加的。目的是:

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

然而,纯粹的性能并不是唯一的收获。它还将防止 HashDoS attack,以防使用哈希映射来存储用户输入,因为用于在桶中存储数据的red-black tree 在 O(log n)。在满足特定条件后使用树 - 请参阅Eugene's answer。

【讨论】:

【参考方案6】:

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

真正的哈希函数必须遵循这个规则——

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

【讨论】:

这不能回答问题。

以上是关于HashMap Java 8 实现的主要内容,如果未能解决你的问题,请参考以下文章

Java中HashMap底层实现原理(JDK1.8)源码分析

(转载)Java 8 认识 HashMap

Java面试必问之Hashmap底层实现原理(JDK1.8)

Java 揭秘 HashMap 实现原理(Java 8)

Java中HashMap底层实现原理(JDK1.8)源码分析

「Java并发」 HashMap实现原理及源码分析(Java 1.8.0_101)