【问题标题】:Strange behavior of HashMap in JavaJava中HashMap的奇怪行为
【发布时间】:2017-05-16 10:02:32
【问题描述】:

我在下面的课程中发现了 hashmap 的一些奇怪行为。

class Employee {

  private String a;
  private int b;

  public Employee(String a, int b) {
    this.a = a;
    this.b = b;
  }

  @Override
  public int hashCode() {
    final int prime = 31;
    int result = 1;
    result = prime * result + ((a == null) ? 0 : a.hashCode());
    result = prime * result + b;
    return result;
  }

  @Override
  public boolean equals(Object obj) {
    if (this == obj)
        return true;
    if (obj == null)
        return false;
    if (getClass() != obj.getClass())
        return false;
    Employee other = (Employee) obj;
    if (a == null) {
        if (other.a != null)
            return false;
    } else if (!a.equals(other.a))
        return false;
    if (b != other.b)
        return false;

    return true;
  }

  public static void main(String[] args) {
    HashMap<Employee,Integer> map = new HashMap<>();

    for(int i = 0; i < 13; i++) {
        map.put(new Employee( i + "", i), i + i);
    }
  }
}

当我使用 new Employee("", i) 作为在地图中存储数据的键时,它工作正常并在第 12 个节点插入后调整地图大小。但在使用 new Employee(i+"", i) 作为键,它表现出奇怪的行为,在使用此键添加第 10 个元素时,它将映射的大小从 16 调整为 32,在添加第 11 个元素时,它再次将映射的大小从 32 调整为 64。 如果您发现此行为的任何原因,请提供帮助。

【问题讨论】:

  • 您如何观察这种行为?用调试器?您可能在谈论内部大小而不是地图中的实际条目数,对吧?
  • 是的,在调试模式下,您可以检查添加第 10 个元素时它的大小从 16 调整到 32...

标签: java hashmap hashcode


【解决方案1】:

原因 - Java 8 中 HashMap 的新组织。当特定 bin 内的列表变得太长时,HashMap 将该列表迁移到树而不是链表 - 称为 treeifying 的过程。

TREEIFY_THRESHOLD = 8 表示当在给定的 bin 中有 8 个条目时,给定的 bin 应该在二叉树中存储冲突值而不是链表(从而从 O(n)O(log n)

if (binCount >= TREEIFY_THRESHOLD - 1)
                treeifyBin(tab, hash);

方法 treeifyBin 将 bin 中的所有链接节点替换为哈希,除非表太小,在这种情况下它会调整表的大小;

因此,在您的情况下,您将获得 64 大小(此代码会调整大小两次,将选项卡大小增加到 32 和 64 (MIN_TREEIFY_CAPACITY)):

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

【讨论】:

    【解决方案2】:

    正如@G_H 提到的,我相信您指的是地图的内部结构,可以通过调试器看到。 HashMap 使用 hashCode() 方法对“桶”中的对象进行分组。

    您已覆盖的 hashCode() 方法使用字符串成员 a 的值。当 a 为 "" 时,其哈希码为 0,因此与将 a 设置为具有更高哈希码的更有意义的字符串时相比,您插入的元素的哈希码彼此之间的距离相对较近。

    由于某种原因(取决于哈希桶的具体实现方式),HashMap 对象决定在哈希码值距离较远时更快地扩大其内部结构。

    对于你插入的元素,看看它们的哈希码值,就会有意义。

    【讨论】:

    • 实际上是“更好的传播元素”(如字符串 a == i + "" 中的元素)导致地图调整大小两次。正如@Olga 发现的那样,这是由于 treefying 开始了,但我想知道为什么所有这些值都会导致 map 将它们放在一个 bin 中。
    【解决方案3】:

    我尝试重写代码的 has 方法。通过您的实现(使用反射获取地图详细信息),

    Loop 0 : Capacity : 16, Factor : 12, Current Size : 1
    Loop 1 : Capacity : 16, Factor : 12, Current Size : 2
    Loop 2 : Capacity : 16, Factor : 12, Current Size : 3
    Loop 3 : Capacity : 16, Factor : 12, Current Size : 4
    Loop 4 : Capacity : 16, Factor : 12, Current Size : 5
    Loop 5 : Capacity : 16, Factor : 12, Current Size : 6
    Loop 6 : Capacity : 16, Factor : 12, Current Size : 7
    Loop 7 : Capacity : 16, Factor : 12, Current Size : 8
    Loop 8 : Capacity : 32, Factor : 24, Current Size : 9
    Loop 9 : Capacity : 64, Factor : 48, Current Size : 10
    Loop 10 : Capacity : 64, Factor : 48, Current Size : 11
    Loop 11 : Capacity : 64, Factor : 48, Current Size : 12
    Loop 12 : Capacity : 64, Factor : 48, Current Size : 13
    

    如下重写后,

    public int hashCode() {
        final int prime = 31;
        return prime * this.b;
    }
    

    那么大小的增加就如预期的那样,

    Loop 0 : Capacity : 16, Factor : 12, Current Size : 1
    Loop 1 : Capacity : 16, Factor : 12, Current Size : 2
    Loop 2 : Capacity : 16, Factor : 12, Current Size : 3
    Loop 3 : Capacity : 16, Factor : 12, Current Size : 4
    Loop 4 : Capacity : 16, Factor : 12, Current Size : 5
    Loop 5 : Capacity : 16, Factor : 12, Current Size : 6
    Loop 6 : Capacity : 16, Factor : 12, Current Size : 7
    Loop 7 : Capacity : 16, Factor : 12, Current Size : 8
    Loop 8 : Capacity : 16, Factor : 12, Current Size : 9
    Loop 9 : Capacity : 16, Factor : 12, Current Size : 10
    Loop 10 : Capacity : 16, Factor : 12, Current Size : 11
    Loop 11 : Capacity : 16, Factor : 12, Current Size : 12
    Loop 12 : Capacity : 32, Factor : 24, Current Size : 13
    

    虽然我不能肯定地解释,但下面来自 HashMap 实现的 sn-p 表明哈希计算值可能会增加 Map 的大小。

    void More addEntry(int hash, K key, V value, int **bucketIndex**) {
        Entry<K,V> e = table[bucketIndex];
        table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
        if (size++ >= threshold)
            resize(2 * table.length);
    }
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2023-03-28
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2023-04-06
      • 2018-08-15
      相关资源
      最近更新 更多