【问题标题】:Confusion reagrding Java HashMap collision关于 Java HashMap 冲突的困惑
【发布时间】:2020-09-22 14:47:45
【问题描述】:

我有一个名为 Department 的自定义类,其中 equals 和 hashCode 都被覆盖了。请找到如下的sn-p:

class Department {
    private final int id;
    private final String name;
    private final int count;

    public Department(int id, String name, int count) {
        super();
        this.id = id;
        this.name = name;
        this.count = count;
    }

    @Override
    public boolean equals(Object obj) {
        if (obj == null)
            return false;
        if (!(obj instanceof Department))
            return false;

        final Department emp = (Department) obj;

        return emp.name != null && emp.name.equals(name) && emp.count == count && emp.id == id;
    }

    @Override
    public int hashCode() {
        return count + name.length();
    }

    @Override
    public String toString() {
        return "ID: " + id + ", Name: " + name + ", Age: " + count + ", hashCode: " + hashCode();
    }
}

在 main 方法中,我以这样的方式初始化了两个部门,它们的 equals 将返回 false 但具有相同的哈希码。然后将这两个部门添加到 HashMap 中。请找到以下主要方法调用:

public static void main(String[] args) {
        final Department dep1 = new Department(1, "software", 35);
        final Department dep2 = new Department(2, "software", 35);
        System.out.println("\n\nIs dep1.equals(dep2)? -- " + dep1.equals(dep2));
        System.out.println("Is dep1==dep2? -- " + (dep1 == dep2));

        System.out.println("\n\nDepartment 1: " + dep1);
        System.out.println("Department 2: " + dep2);

        final HashMap<Department, String> departmentHashMap = new HashMap<>();
        departmentHashMap.put(dep1, "Software 1");
        System.out.println("\n\nDepartment 1 added to map");
        System.out.println("Is Department 2 available in map? -- " + departmentHashMap.get(dep2));
        System.out.println("Is Department 2 key available in map? -- " + departmentHashMap.containsKey(dep2));
        departmentHashMap.put(dep2, "Software 2");

        System.out.println("\n\nDepartment 1: " + departmentHashMap.get(dep1));
        System.out.println("Department 2: " + departmentHashMap.get(dep2));

        for (final Entry<Department, String> entry : departmentHashMap.entrySet()) {
            System.out.println("Key: " + entry.getKey() + ", Value: " + entry.getValue());
        }
    }

根据文档,当两个不同的条目具有相同的hashcode但不满足equals比较时,会导致HashMap发生冲突,条目将存储为链表。我没有观察到这种特殊行为。但是当我遍历 HashMap 条目时,它们被作为单独的条目获取,而不是链表。请找到如下输出:

Is dep1.equals(dep2)? -- false
Is dep1==dep2? -- false


Department 1: ID: 1, Name: software, Age: 35, hashCode: 43
Department 2: ID: 2, Name: software, Age: 35, hashCode: 43


Department 1 added to map
Is Department 2 available in map? -- null
Is Department 2 key available in map? -- false


Department 1: Software 1
Department 2: Software 2
Key: ID: 1, Name: software, Age: 35, hashCode: 43, Value: Software 1
Key: ID: 2, Name: software, Age: 35, hashCode: 43, Value: Software 2

我无法在任何地方引用举例说明这种特殊情况。任何有助于澄清概念的帮助都将受到高度赞赏。

【问题讨论】:

  • 请链接“文件”! “实施的真相”可以在source code 中找到。当然所有“冲突注意事项”都是“内部的”,不会影响外部 api (entrySet())
  • 在内部,它存储键/值对。因此,当发生碰撞时,它们会被添加到同一个存储桶中的“列表”中。迭代此列表以找到适当的匹配键,以便使用与其关联的值。换句话说,在地图之外,没有“LinkedList”机制的概念,这是一个被封装的实现细节。

标签: java hashmap equals hashcode


【解决方案1】:

我会尝试带你进入 Associative Array ADT 的深度之旅,其实现是有问题的数据结构 - HashMap / HashTable

我会尽量给出一些足够清晰的学术和理论背景,以便您更好地掌握这个主题。

HashMapAssociative Array 抽象数据类型 (ADT) 的一种实现,这种 ADT 最常作为Hash Table 数据结构实现。因此,您可以将 HashMapHashTable 视为概念上相同的数据结构,尤其是在 Java 中,其中只有次要的 DS 特性级别实现(如线程安全、并发、排序等) .) 不同。

Hash Table(以及HashMap,我将在下文中交替使用这两个结构名称),数据结构最重要的特点是它给你Ө(1) 读取、插入和更新操作的时间,通过在内部实现关联数据结构,并感谢 散列函数 H(x) 思想。

Hash Function 是哈希表中的一个基本概念。它在底层实现中通过Index Normalization 进行计算和归一化。

Hash Table 在底层是由它的后备数组实现的。该支持数组存储(类型):

  1. 哈希表的实际条目,因此,该支持数组属于哈希表的特定条目类型 - Entry&lt;K, V&gt;[]。 (通常,哈希表的 Entry 是一个特殊的类型/类,它包含那个键和那个值的组合——即它代表一个条目,并且它的实例保存在后备数组中;
  2. 哈希表条目的桶。现在,请密切注意这里,因为我正在非常深入地解释这一点。在这种情况下,数组的类型是 Bucket,而每个桶又将是辅助数据结构的一个实例,通常是 >链接列表。所以,长话短说 - 在这种情况下,您可以想象后备数组,它将类似于 LinkedList&lt;K, V&gt;[]

现在,我们准备介绍碰撞



碰撞

Hash Function H(x) 的一个重要属性是,它必须是确定性统一。一个好的均匀 H(x) 可以降低碰撞概率 - 这意味着 H(x) 不太可能将两个不同的输入散列到相同的输出,然而这可能会发生! 对于两个不同的输入,您可能会得到相同的输出,这些输出将被归一化为相同的数字,并有效地指向支持数组的相同槽。

所以,这是一个冲突 - 当两个输入散列到同一个索引时。

问:如何处理? A:解决这个问题有两种技术策略。

  1. 独立链接
  2. 开放寻址

由于您的问题涉及存储 List 实现的后备数组,因此这是一种单独链接策略,我会告诉你几句话(如果你会发现我的答案很有用,我以后可能还会添加对线性探测的解释)。



分离链

Separate Chaining - 通过维护辅助数据结构(通常是链表,但也可以使用其他数据结构)来处理碰撞,以保存所有的碰撞。那些散列到相同特定散列值的不同键。 (保存碰撞键的辅助数据结构,有时称为 Bucket 表示许多元素的集合)

在这个策略/技术中,正如我上面所说,支持数组的每个元素都是Linked List(哈希表条目的)数据结构,并且每当两个或多个元素(键)碰撞(哈希到相同哈希值),它们的条目只是添加到相应的链接列表中(放置在碰撞哈希值的位置),但仅当这些条目具有不同的原始(哈希之前)键。如果哈希后两个条目的键发生冲突,并且这些条目的原始键也相等,则现有条目将被我们添加的条目替换。 例如,如果 Hash Table 包含 {3, "Subhrat"} 条目,并且我们要再添加一个条目 {5, “David”},但由于散列函数不佳,3 和 5 散列为相同的值 x,那么后一个元素将刚刚添加到相应的链表(在后备数组的索引x);但是,如果两个键散列到相同的值,并且它们在原始状态(散列之前)也相等,则现有条目将被后者替换。

现在是你没有观察到的部分。

:在分离链的情况下如何进行查找?

  1. 我们给出哈希表的密钥;
  2. 键被散列,结果值代表后备数组的索引;
  3. 数组中第二步的对应槽有一个桶(在我们的例子中是链接列表),并且在该桶中查找/搜索原始键(第一步)。

我希望这有助于了解 Hash MapHash Table 的工作原理,现在您可以更清楚为什么看不到 LinkedList 被提取出来了。

【讨论】:

  • 如果对您有帮助,请随时接受并投票赞成这个答案。
【解决方案2】:

您创建的示例很好。在内部,哈希图中将有一个条目,它是一个链表。 但是,如果条目是链表,则无法从外部检查,即使用 Map API。 Map 及其迭代器的合同规定,它将单独交付所有项目,而不是按特定顺序交付。
查看 Java 源代码以了解迭代器在内部是如何工作的。

【讨论】:

    【解决方案3】:

    从您的实现中,dept1 和 dept2 将作为链表或(可能来自 JDK8 的 TreeMap)维护在 HashMap 的同一存储桶中。 dept1,dept2 将进入同一个存储桶的原因是因为它们具有相同的 hashCode() 。所以会有碰撞。

    根据您的问题,您将无法检查 HashMap 的内部结构,因为元素是如何存储在链表或 TreeMap 中的?因为没有公开的 API 是正确的。这将是一个泄漏的抽象。

    在非常高的级别上,Map.entrySet() 迭代器从存储桶 0 扫描 HashMap,扫描每个存储桶的链表(或 TreeMap)并递归地对每个存储桶执行相同操作,从而在不告诉我们的情况下迭代每个条目内部结构

    【讨论】:

      【解决方案4】:

      为什么Equals() 为假?因为你比较了每个属性,id 是

      不同所以输出是false

      为什么我在遍历条目时没有得到 LinkedList? 当您循环时,您使用 EntryIterator 逐个节点读取

      ,如果你想看LinkedList你可以使用Mockito

      package com.example;
      
      import java.util.HashMap;
      import org.junit.Test;
      import org.junit.runner.RunWith;
      import org.mockito.internal.util.reflection.Whitebox;
      import org.mockito.runners.MockitoJUnitRunner;
      
      @RunWith(MockitoJUnitRunner.class)
      public class ExampleClassTest {
      
          static class Department {
      
              private final int id;
              private final String name;
              private final int count;
      
              public Department(int id, String name, int count) {
                  super();
                  this.id = id;
                  this.name = name;
                  this.count = count;
              }
      
              @Override
              public boolean equals(Object obj) {
                  if (obj == null) {
                      return false;
                  }
                  if (!(obj instanceof Department)) {
                      return false;
                  }
      
                  final Department emp = (Department) obj;
      
                  return emp.name != null && emp.name.equals(name) && emp.count == count && emp.id == id;
              }
      
              @Override
              public int hashCode() {
                  return count + name.length();
              }
      
              @Override
              public String toString() {
                  return "ID: " + id + ", Name: " + name + ", Age: " + count + ", hashCode: " + hashCode();
              }
          }
      
          @Test
          public void shouldPrintCollision() {
      
              final Department dep1 = new Department(1, "software", 35);
              final Department dep2 = new Department(2, "software", 35);
      
              final HashMap<Department, String> departmentHashMap = new HashMap<>();
              departmentHashMap.put(dep1, "Software 1");
              departmentHashMap.put(dep2, "Software 2");
      
              Object[] array = (Object[]) Whitebox.getInternalState(departmentHashMap, "table");
              Object firstNode = null;
              for (Object obj : array) {
                  if (obj != null) {
                      firstNode = obj;
                  }
              }
      
              printRecusive(firstNode);
          }
      
          private void printRecusive(Object node) {
              if (node == null) {
                  return;
              }
              System.out.println(node);
              Object next = Whitebox.getInternalState(node, "next");
              printRecusive(next);
          }
      }
      
      

      ,输出

      ID: 1, Name: software, Age: 35, hashCode: 43=Software 1
      ID: 2, Name: software, Age: 35, hashCode: 43=Software 2
      

      【讨论】:

        【解决方案5】:

        在学术层面,散列容器可以通过多种方式处理冲突,但基本上桶可以是指向单个项目的指针/引用,也可以是某种辅助容器。两种口味都有其优势和成本。

        • 如果它是辅助容器,则所有命中都将进入该容器以进行该容器支持的任何类型的搜索;可以在第一个桶添加时创建容器。
        • 如果指针/引用不为空,则命中必须进入由特定序列选择的其他空桶:线性、二次、双哈希,您可以在 Google 上搜索更多信息——闻起来像一个流行的论文主题。使用指针/引用哈希容器,未命中搜索会继续进行,直到找到空存储桶。

        JAVA 使用链表辅助容器。由于散列容器没有排序,二级容器的顺序并不重要,所以这是一个明智的选择:添加便宜且搜索线性。迭代器也不必担心哈希映射的顺序,只要每个元素对都被迭代。

        使用任何类型的哈希容器的聪明之处在于将初始大小设置得非常大,足够大,这样就很少发生冲突。空桶是一个指针/引用,8 字节,但每次添加这些桶类型时,辅助容器的开销都会增加,因此它是基本空间与速度的权衡!我推测非 mod-2 大小可能会更好,尽管 mod-2 大小可能会更快地划分(并且会这样做),并且 素数 似乎特别适合随机化存储桶选择。

        不言而喻,hash函数也应该尽可能随机

        一些散列容器的大小可以翻倍,但是在桶列表被复制两倍之后,一半的项目在错误的桶中,所以没有免费的午餐。在它被清理之前,在迭代时,所有存储桶项目都必须检查其哈希值,在 find 时将进行更多存储桶搜索,并且可能在添加时即兴清理。 JAVA 似乎没有可扩展的哈希容器。

        【讨论】:

          【解决方案6】:

          请考虑当我们谈论编程时有两个不同的概念,实现和抽象。在这种情况下,当您在 Hashmap 中讨论 LinkedList 时,此 LinkedList 用于 Hashmap 的内部实现,这意味着当 HashMap 在内部接收具有相同哈希码的两个键时,它将这些条目(具有相同的哈希码)存储在相同的 LinkedList 中,但除非您通过实现此行为的 HashMap 中的代码,否则您无法以该 api 的用户身份看到此内部实现。

          另一方面,当您在主要方法中测试 HashMap 时,您实际上是在测试 HashMap api 的外部表示,这正是每个 HashMap 用户所期望的。他们期望当他们将一个带有键的元素放入 HashMap 时,然后他们可以请求 HashMap 以使用相同的键获取该元素(这里相同意味着两个键相等)注意键的哈希码对于HashMap 的用户(这句话只在功能方面是正确的,而不是在性能方面是正确的)。作为 HashMap 的用户,您应该知道的唯一规则是,当两个键相等时,它们必须具有相同的 hashCode。

          这里的 hashCode 用于性能而非功能。想象一下,即使在这种情况下 HashMap 工作正常,您的 hashCode 总是为 Department 的所有实例返回固定整数(例如 1)。但是在这种情况下,您的所有元素都存储在内部实现的一个列表中,这非常慢。为了使这更复杂,您可以将 String 和 Object 视为 HashMap 中的键。

          但是为什么 HashMap 在其内部实现中使用 LinkedList 呢?长话短说,当我们谈论数据结构时,数组适合随机访问,但它们需要大量内存。假设您的密钥是整数类型,您可以使用数组来存储元素,但是这样您应该有一个 lentgh 2147483647 的数组(在这种情况下让我们放负数)但是这样您就可以通过键入( O1)。另一种方法是以这种方式使用 LinkedList,您应该将带有值的键存储在 LinkedList 的条目中。这样,您的内存使用量就很少,因为您在 LinkedList 的末尾分配内存(当新条目到达时);但是,这种方法的缺点是它的性能,因为当您想通过键查找元素时,您应该遍历 LinkedList 中的所有元素,这非常慢。 HashMap 实现通过混合数组和 LinkedList 尽了最大的努力来两全其美。

          简而言之,此实现减少了使用 hashCode 所需的数组大小。它尝试使用散列码在其内部数组中分派元素,并允许不同的键具有相同的散列码,因此我们不需要一个具有所有可能值大小的数组。 所以通过这个实现我们可以有更小的数组。但是在发生冲突的情况下(当两个键具有相同的 hashCode 时)它们存储在数组的同一个条目中,实际上数组的每个条目都是一个linkedList。当我们从 Hashmap 请求一个元素并为其提供键时,hashmap 首先通过计算该键的哈希码来查找数组条目并找到该条目(实际上是一个linkedList),然后遍历该 LinkedList 并为该 LinkedList 的每个元素计算相等,直到它找到等于提供的键的元素。这样我们就有了性能和小内存分配。

          【讨论】:

            猜你喜欢
            • 2016-10-26
            • 2012-12-23
            • 2016-03-14
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2019-05-17
            • 2020-03-06
            相关资源
            最近更新 更多