在返回常量哈希码的情况下,Java8 Hashmap 重新散列

Posted

技术标签:

【中文标题】在返回常量哈希码的情况下,Java8 Hashmap 重新散列【英文标题】:Java8 Hashmap rehashing in case of returning constant hashcode 【发布时间】:2019-07-09 12:38:11 【问题描述】:

根据下面的代码,hashmap 初始默认容量为 16,LF 为 0.75,所以当我输入第 13 个元素时,应该进行重新散列,因为我提供了一个恒定的散列码,它在内部维护一个链表来维护插入顺序。所以,直到第 10 个元素它按预期工作,但是当我输入第 11 个元素时,它会打乱顺序。 任何人都可以帮助我理解为什么它只在第 11 个元素插入时发生。

class A
    int a;

    A(int a)
        this.a = a;
    
    @Override
    public int hashCode() 
        return 7;
    
    @Override
    public String toString() 
        return "" + a + "";
    

class Base 
    public static void main(String[] args) 
        Map<Object, Integer> m = new HashMap<Object, Integer>();
        m.put(new A(1), 1);
        m.put(new A(2), 1);
        m.put(new A(3), 1);
        m.put(new A(4), 1);
        m.put(new A(5), 1);
        m.put(new A(6), 1);
        m.put(new A(7), 1);
        m.put(new A(8), 1);
        m.put(new A(9), 1);
        m.put(new A(10), 1);
        //m.put(new A(11), 1);
        System.out.println(m);
    

直到第 10 个元素我得到的输出:

1=1, 2=1, 3=1, 4=1, 5=1, 6=1, 7=1, 8=1, 9=1, 10=1

输入第 11 个元素后得到的输出:

4=1, 1=1, 2=1, 3=1, 5=1, 6=1, 7=1, 8=1, 9=1, 10=1, 11=1

它应该保持插入顺序,或者如果它在内部使用 RB 树,那么在这种情况下它在这里使用哪种遍历?

【问题讨论】:

我认为是您输入的第 12 个元素决定了增加容量。 您是否单步执行了 HashMap 代码?你在那里发现了什么意想不到的东西? 如果你想要恒定的迭代顺序,试试LinkedHashMap。它只是HashMap,带有一个额外的节点链表,允许您按插入顺序进行迭代。 @VinayPrajapati 是的,但是当您输入第 13 个元素时,应该会发生重新散列。 @RolandIllig 我不明白你到底想说什么。请提供更清晰的说明。 【参考方案1】:

它应该保持插入顺序,或者如果它在内部使用 RB 树,那么在这种情况下它在这里使用哪种遍历?

没有“应该”; HashMap 不保证任何订单。当前实现中实际发生的情况由两个常量决定,TREEIFY_THRESHOLD = 8MIN_TREEIFY_CAPACITY = 64

当一个bucket中的item数量超过前者时,bucket会转化为一棵节点树,除非map的总容量小于后者的常数,在这种情况下,容量会翻倍。

所以当你插入第 9 个对象时,容量会从 16 提升到 32,插入第 10 个会导致从 32 提升到 64,然后,插入第 11 个元素会导致桶转换为树。

无论是否有实际好处,这种转化总会发生。由于对象具有相同的哈希码并且没有实现Comparable,因此最终将使用它们的身份哈希码来确定顺序。这可能会导致顺序发生变化(在我的环境中,它不会)。

【讨论】:

另外值得强调的是,所有这些行为都是实现细节,而不是公共接口的一部分。因此它可能会发生变化,您的代码不应假设或依赖它 谢谢..@Holger 所以它将在第 11 个元素而不是第 9 个元素处转换为树? @gidds 当然。它是特定于实现的,仅适用于这个特定的极端情况。这就是为什么我的回答以“HashMap 不保证任何顺序”开头。【参考方案2】:

它独立于指定的哈希码编号,即 7,而是您的 hascode 是恒定的导致它。原因如下:

我浏览了 HashMap 的 put 方法的源码,有一个常量TREEIFY_THRESHOLD 决定何时将普通桶转换为树。

静态最终整数 TREEIFY_THRESHOLD = 8;

put方法的sn-p代码如下(Put方法调用putVal方法):

.
.
.

                for (int binCount = 0; ; ++binCount) 

                    if ((e = p.next) == null) 

                        p.next = newNode(hash, key, value, null);

                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st

                            treeifyBin(tab, hash);

                        break;

                    
.
.
.

记下包含if (binCount &gt;= TREEIFY_THRESHOLD - 1) 条件的行。一旦发现一个桶的容量达到TREEIFY_THRESHOLD,它就会调用treeifyBin()方法。

只有在遇到MIN_TREEIFY_CAPACITY 时,此方法才会依次调用resize() 方法。

 final void treeifyBin(Node<K,V>[] tab, int hash) 
        int n, index; Node<K,V> e;
        if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
            resize();
        else if ((e = tab[index = (n - 1) & hash]) != null) 
            TreeNode<K,V> hd = null, tl = null;
            do 
                TreeNode<K,V> p = replacementTreeNode(e, null);
                if (tl == null)
                    hd = p;
                else 
                    p.prev = tl;
                    tl.next = p;
                
                tl = p;

             while ((e = e.next) != null);
            if ((tab[index] = hd) != null)
                hd.treeify(tab);
        
    

在上面的sn-p中查找以下条件

if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
            resize();

resize 方法然后根据它具有的多个条件检查相应地增加地图的大小。它基本上通过负载系数增加容量。

如果没有,就像树化一样。树中的元素数量减少。使用 UNTREEIFY_THRESHOLD 即 6 作为基数执行 Untreeify 操作。

我引用了this 链接来查看Hashmap 代码。

【讨论】:

是..完全同意..您对Holger给出的上述回答满意吗? 是的!除了我怀疑容量翻倍。我没有进一步调整代码大小。但我认为应该是 load_factor*capacity 而不是 2*capacity。 @VinayPrajapati 您认为这是基于什么?加载因子的用途在the API documentation 中有详细说明:“加载因子是衡量哈希表在其容量自动增加之前允许达到的程度”。它甚至说,“当哈希表中的条目数超过负载因子和当前容量的乘积时,哈希表被重新哈希……使得哈希表的桶数大约是两倍”。在这个实现中,它正好是两倍。

以上是关于在返回常量哈希码的情况下,Java8 Hashmap 重新散列的主要内容,如果未能解决你的问题,请参考以下文章

Java8系列之重新认识HashMap

为啥 Java 8 中的哈希映射使用二叉树而不是链表? [关闭]

java集合-补充HashMapJDK1.8

Java 中哈希码的说明

如何在不欺骗编译器的情况下保持这个常量正确?

Java8 Hash改进/内存改进