【问题标题】:how does hashing in java works?java中的散列是如何工作的?
【发布时间】:2013-06-19 16:11:42
【问题描述】:

我正在尝试找出有关 java 中的散列的一些事情。 例如,如果我想在哈希图中存储一些数据,它会有某种带有哈希值的底层哈希表吗? 或者,如果有人能对散列的工作原理给出一个简单明了的解释,我将不胜感激。

【问题讨论】:

  • 据我了解,散列(在您使用它的上下文中)会产生一个值,该值对于两个相等的对象来说是相等的,而对于两个不相等的对象来说不太可能相等平等的。这加速了 .contains() 和其他调用 .equals() 的方法的问题。一个简单的例子是,如果一个类有成员 x 和 y,那么哈希码可能是 71*(x+71*y),但是当你覆盖 equals 时,IDE 会为你生成一个好的哈希
  • 但是哈希码在计算之后到底去哪里了?它是否存储在某种底层哈希表中?
  • 你懂hashtables吗?这不是是刻薄的评论;说真的,如果你真的想了解散列是如何工作的,你需要了解数据结构。 Wikipedia article 很棒,The Algorithm Design Manual 中的解释也是如此。
  • 我会回答这个问题,但维基百科已经有一个关于一般数据结构意义上的哈希表的优秀页面 - en.wikipedia.org/wiki/Hash_table
  • 非常感谢!希望我能解决

标签: java algorithm hash hashcode


【解决方案1】:

HashMap 基本上在内部实现为 Entry[] 数组。如果你了解什么是linkedList,那么这个Entry 类型只不过是一个linkedlist 的实现。这种类型实际上存储了键和值。

要将元素插入数组,您需要索引。你如何计算指数?这就是散列函数(hashFunction)发挥作用的地方。在这里,您将一个整数传递给此哈希函数。现在为了获取这个整数,java 调用了对象的 hashCode 方法,该对象被添加为映射中的键。 这个概念称为 preHashing。

现在,一旦知道索引,您就可以将元素放在该索引上。这基本上被称为 BUCKET ,所以如果元素被插入到 Entry[0],你就说它落入了 0 号桶。

现在假设 hashFunction 返回相同的索引,例如 0,用于您想要作为键插入到映射中的另一个对象。这就是 equals 方法被调用的地方,如果 even equals 返回 true,这很简单意味着存在 hashCollision。所以在这种情况下,由于 Entry 是一个链表实现,在这个索引本身上,在这个索引上已经可用的条目上,你再向这个链表添加一个节点(条目)。所以底线是,在 hashColission 上,通过链表在特定索引处有多个元素。

当您谈论从地图中获取密钥时,应用相同的情况。根据 hashFunction 返回的索引,如果只有一个条目,则返回该条目,否则在条目的链表上,调用 equals 方法。

希望这有助于了解其工作原理:)

【讨论】:

    【解决方案2】:

    Java 中的哈希值是由对象通过在Object 类中声明的public int hashCode() 的实现来提供的,它适用于所有基本数据类型。在自定义数据对象中实现该方法后,您无需担心如何在 Java 提供的各种数据结构中使用这些方法。

    注意:实现该方法还需要以一致的方式实现public boolean equals(Object o)

    【讨论】:

      【解决方案3】:

      例如,如果我想在哈希图中存储一些数据,它是否会有某种带有哈希值的底层哈希表?

      HashMap哈希表的一种形式(HashTable 是另一种形式)。它们通过使用HashMaps 键类型提供的hashCode()equals(Object) 方法工作。根据您希望键的行为方式,您可以使用由java.lang.Object 实现的hashCode / equals 方法......或者您可以覆盖它们。

      或者,如果有人能对散列的工作原理给出一个简单明了的解释,我将不胜感激。

      我建议您阅读Hash Tables 上的维基百科页面以了解它们的工作原理。 (FWIW,HashMapHashTable 类使用“带有链表的单独链接”,以及一些其他调整来优化平均性能。)

      哈希函数通过将对象(即“键”)转换为整数来工作。如何做到这一点取决于实施者。但一种常见的方法是组合对象字段的哈希码,如下所示:

        hashcode = (..((field1.hashcode * prime) + field2.hashcode) * prime + ...)
      

      其中prime 是一个较小的素数,例如31。关键是您可以获得不同键的散列码值的良好分布。你不想要的是很多键都散列到相同的值。这会导致“冲突”并且对性能不利。

      当您实现hashcodeequals 方法时,您需要以满足以下约束的方式执行此操作,以使哈希表正常工作:

       1. O1.equals(o2) => o1.hashcode() == o2.hashcode()
       2. o2.equals(o2) == o2.equals(o1)
       3. The hashcode of an object doesn't change while it is a key in a hash table.
      

      另外值得注意的是Object提供的默认hashCodeequals方法是基于目标对象的身份的。


      “那么哈希值存储在哪里呢?它不是HashMap的一部分,所以有一个与HashMap关联的数组吗?”

      散列值通常存储。而是根据需要计算

      HashMap 类的情况下,每个键的哈希码实际上缓存在条目的Node.hash 字段中。但这是一种性能优化......以使哈希链搜索更快,并在哈希表调整大小时避免重新计算哈希。但是如果你想要这种程度的理解,你真的需要阅读源代码而不是提问。

      【讨论】:

      • 但是哈希值存储在哪里呢?它不是 HashMap 的一部分,那么有没有关联到 HashMap 的数组?
      【解决方案4】:

      这是 Java 中最基本的合约:the .equals()/.hashCode() contract。 这里最重要的部分是两个被认为是.equals() 的对象应该返回相同的.hashCode()

      反之则不然:不相等的对象可能返回相同的哈希码。但它应该尽可能少发生。考虑下面的.hashCode() 实现,虽然它完全合法,但它的实现已经被破坏了:

      @Override
      public int hashCode() { return 42; } // legal!!
      

      虽然这个实现遵守约定,但它几乎没用......因此,一个好的散列函数一开始就很重要。

      现在:Set 合约规定Set 不应包含重复元素;然而,Set 实现的策略被留下了......好吧,到实现。如果查看Map 的javadoc,您会注意到,可以通过称为.keySet() 的方法检索其密钥。所以MapSet在这方面关系非常密切。

      如果我们以HashSet(最终是HashMap)为例,它依赖于.equals().hashCode():添加一个项目时,它首先计算这个项目的哈希码,并根据此哈希码尝试将项目插入到给定的存储桶中。相比之下,TreeSet(和TreeMap)依赖于元素的自然排序(参见Comparable)。

      然而,如果要插入一个对象并且这个对象的哈希码会触发它插入一个非空的哈希桶(参见上面的合法但损坏的.hashCode() 实现) , 然后.equals() 用于确定该对象是否真的唯一。

      请注意,在内部,HashSetHashMap...

      【讨论】:

      • 我认为equals定义等价关系的要求更为基本,但hashCode函数非常重要。说hashCode 应该为不同的对象返回不同的数字可能会有所帮助,因为它可以快速完成,并且不违反匹配值必须具有匹配哈希的要求。发生的每一次散列冲突都会减慢散列集合查找方法的速度。如果一个包含 50,000 个项目的集合有十几个碰撞,则稍微减速。如果集合中的 20,000 个项目都返回相同的哈希值,那么速度会大大降低。
      • @supercat 是的,我知道,我已经举例说明了该做什么;)
      • 返回一个在每次调用时递增的静态计数器将比返回 42 更糟糕。在某些情况下,如果一个类型实现可变值相等,则返回一个常量(尽管可能少一个“流行" 比 42 或 8675309) 可能是明智的。如果一个类型有一个可变的Name 字段,并且如果Name 匹配,则对象被认为是equal,如果在存储在HashSet 中时没有对象的名称发生变化,则让hashCode() 返回name.hasCode() 将起作用,但返回常量hashCode 将允许使用当前名称找到所有对象,即使它们在存储时具有其他名称。
      • @supercat 我知道所有这些......无论如何,如果你依赖于可变字段的.hashCode()/.equals(),你有一个更深层次的问题要处理;)我不有这个问题,因为我创建的所有实例都是不可变的,或者如果它们不是,请将其留给Object....
      • 我只是在评论你的说法,即返回一个常量是“最糟糕的 hashCode 实现”;事实并非如此,而且有时这样做可能是合理的(例如,如果需要将对象存储在包装 hashSet<T> 的类中,但线性搜索的集合类型会更好)。实际上,quickly 返回一个常量甚至不是最破烂的“合法”hashCode 方法;一个需要很长时间才能得出对每个实例都相同的答案的问题会更糟糕[顺便说一句,我认为 .NET 的一种类型意外地做到了这一点]。
      【解决方案5】:

      散列是一种在对其属性应用任何函数/算法后为任何变量/对象分配唯一代码的方法。

      【讨论】:

        【解决方案6】:

        HashMap 在 Map.Entry 静态嵌套类实现中存储键值对。 HashMap 工作于哈希算法,在 put 和 get 方法中使用 hashCode() 和 equals() 方法。

        当我们通过传递键值对调用 put 方法时,HashMap 使用 Key hashCode() 和散列来找出 存储键值对的索引。条目存储在 LinkedList 中,所以如果已经有 现有条目,它使用 equals() 方法检查传递的密钥是否已经存在,如果存在则覆盖 否则它会创建一个新条目并存储此键值条目。

        当我们通过传递 Key 调用 get 方法时,它再次使用 hashCode() 来查找索引 在数组中,然后使用 equals() 方法找到正确的条目并返回它的值。 下图将清楚地解释这些细节。

        关于 HashMap 的其他重要信息是容量、负载因子、阈值调整大小。 HashMap 初始默认容量为 16,加载因子为 0.75。阈值是容量的乘积 通过负载因子以及每当我们尝试添加条目时,如果地图大小大于阈值, HashMap 将 map 的内容重新散列到一个容量更大的新数组中。 容量总是 2 的幂,所以如果你知道需要存储大量的键值对, 例如在缓存数据库中的数据时,最好用正确的容量初始化 HashMap 和负载系数。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2010-11-27
          • 2013-05-18
          • 2014-11-07
          • 2015-08-27
          • 2016-11-02
          • 2018-03-31
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多