当前位置: 首页 > 知识库问答 >
问题:

澄清Java实现HashSet/HashMap背后的事实

锺高翰
2023-03-14

1.我了解不同的哈希映射机制和处理密钥冲突的方式(开放寻址-线性/二次探测、链接、可扩展哈希等)。哈希集/哈希映射使用哪一种?

2.我意识到好的哈希映射依赖于好的哈希函数。Java的HashSet/HashMap如何散列对象?我知道有一个散列函数,但到目前为止,对于字符串,我不需要实现它。如果我现在想对我创建的Java对象进行散列-我需要实现散列函数吗?或者Java有一种内置的创建哈希代码的方法吗?

我知道不能依赖默认实现,因为它将哈希函数建立在不是恒定的内存地址上。

共有2个答案

孟绪
2023-03-14

问题1)

JavaHashMap实现使用链接实现来处理冲突。可以将其视为一个链表数组。

问题2

Object有equals和hashCode的默认实现。equals被实现为返回this==其他hashcode被实现为(所有意图和目的)为每个实例分配一个随机标识符并将其用作hashCode

由于 Java 中的所有类都扩展了 Object,因此它们都继承了这些实现。

默认情况下,某些类覆盖这些实现<正如您提到的,code>String是一个很好的例子。另一个是集合API中的类,因此<code>ArrayList

就实现一个好的哈希码而言,这有点像黑暗艺术。以下是最佳实践的一个很好的总结。

您的最后评论:

我知道不能依赖默认实现,因为它将哈希函数建立在不是恒定的内存地址上。

这是不正确的。< code>hashCode的默认实现是常量,因为它是方法契约的一部分。从Javadoc:

在一个Java应用程序的执行过程中,只要在同一个对象上多次调用hashCode方法,hashCode方法就必须始终返回同一个整数,前提是在对象的equals比较中使用的信息没有被修改。这个整数不需要从一个应用的一次执行到同一应用的另一次执行保持一致。

姬飞昂
2023-03-14

通过阅读HashMap的源代码,您可以自己回答其中的许多问题。

(提示:你通常可以使用Google找到Java SE类的源代码;例如,搜索“java.util.HashMap source”。

我了解不同的哈希映射机制以及处理键冲突的方式(开放寻址-线性/二次探测、链接、可扩展哈希等)。HashSet/HashMap使用哪一个?

锁链。请参阅源代码。(链接到的版本中的第154行)。

Java的HashSet/HashMap如何对对象进行哈希处理?

没有。为此,调用对象的<code>hashCode</code>方法。请参阅源代码。(第360行)。

如果您查看代码,您会发现一些有趣的问题:

>

  • 代码(在我链接到的版本中)使用特殊方法对字符串进行哈希处理。(这似乎是为了允许在平台级别“调整”字符串的哈希。我没有深入研究这个...)

    由< code>Object.hashCode()调用返回的hashcode被进一步“打乱”以减少冲突的机会。(看评论!)

    如果我现在想对我创建的Java对象进行散列-我需要实现散列函数吗?

    你可以做到。

    您是否需要这样做取决于您如何为类定义<code>equals</code>。具体来说,Java的<code>HashMap、<code>hash集和相关类对<code>hashcode()和<code>equals(Object)提出了以下要求:

    1. 如果a.equals(b),则a.hashCode () == b.hashCode()
    2. aHashSet中或者是HashMap中的键时,a.hashCode()返回的值不能改变。
    3. 如果!a.equals(b),那么a.hashCode () == b.hashCode()的概率应该很低,特别是如果ab可能是应用程序的哈希键。

    (性能原因的最后一个要求。如果您的哈希函数“很差”,导致不同键哈希相同的哈希码的可能性很高,那么您会遇到很多冲突。哈希链将变得不平衡,您将无法获得哈希表操作通常期望的平均O(1)性能。在最坏的情况下,性能将是O(N);即相当于链表的线性搜索。)

    或者Java是否有内置的方法来创建哈希代码?

    每个类从<code>对象方法(除非被重写)。它使用所谓的“身份哈希码”;i、 e.基于对象标识(其引用)的哈希值。这与<code>equals(Object)来比较引用。

    我知道不能依赖默认实现,因为它将哈希函数建立在不是恒定的内存地址上。

    这是不正确的。

    默认的hashCode()方法返回“标识hashcode”。这通常基于某个时间点对象的内存地址,但它不是对象的内存地址。

    特别是,如果一个对象被垃圾收集器移动,它的“标识哈希码”保证不会改变。是的。没错,它不会改变......即使对象被移动了!

    (他们如何高效地实现这一点相当聪明。请参阅https://stackoverflow.com/a/3796963/139985有关详细信息。)

    最重要的是,默认的 Object.hashCode() 方法满足我上面列出的所有要求。这是可以信赖的。

  •  类似资料:
    • 我希望下面的代码中有,但它运行良好。 根据JavaDoc for: 这个类的所有“Collection view Methods”返回的迭代器都是快速失败的:如果在迭代器创建后的任何时候,以任何方式(除了通过迭代器自己的remove方法)修改了映射,则迭代器将抛出一个ConcurrentModificationException。 因此,由于我在获得之后修改了,所以我应该得到。为什么不扔?

    • 问题内容: 我了解这是基于实现的,但是在您需要唯一的元素集时使用。那么,为什么在下一个代码中将相同的对象放入地图并进行设置时,两个集合的大小都等于1?地图大小不应该为2吗?因为如果两个集合的大小相等,那么使用这两个集合不会有任何区别。 输出为1和1。 问题答案: 该地图拥有唯一键。当您使用映射中存在的键进行调用时,该键下的对象将被新对象替换。因此大小为1。 两者之间的区别应该很明显: 在您存储键值

    • 我正纠结于一个关于Flink的Kafka的消费者连接器的事件时间的问题。引用Flink doc 自从Apache Kafka 0.10+以来,Kafka的消息可以携带时间戳,指示事件发生的时间(参见Apache Flink中的“事件时间”)或消息被写入Kafka代理的时间。 Kafka消费者不会发出水印。 一些问题和问题浮现在我的脑海中: > 我如何知道它的时间戳是发生的时间还是写给Kafka经纪

    • 所以这段时间我一直认为递归的问题是理解案例。事实证明,我的问题是理解递归案例的值。例如,向后打印数组的一部分。 原始尝试 一次有效的尝试 然而,这是一种高效的递归吗?还是有更好的办法?这是我从写出来的时候看出来的唯一方法。

    • 问题内容: 看一下Java 6的源代码,实际上是通过使用Set的每个条目上的伪对象实例来实现的。 我认为这浪费了4字节(在32位计算机上)用于条目本身的大小。 但是,为什么仍然使用它呢?除了使代码维护更容易之外,还有什么理由要使用它? 问题答案: 实际上,不只是。 Java 6中该接口的 所有 实现都基于底层。这不是必需的;这只是实现的方式。您可以通过查阅有关的各种实现的文档来自己查看。 您的主要

    • 我正在学习一些Java课程,老师开始介绍IO在Java中的工作方式。我只是有几个问题,一个有经验的Java程序员可以澄清。 下面的代码段是一个程序,它在我正在编写代码的同一个文件目录中创建一个(记事本)文本文件。之后,它只需将基本的文本行打印到该文件中。 问题1:由于老师只是简单的解释,这行代码对我来说有点困惑。我知道在这一行中,我们正在创建“outfile”对象。之后,我们调用PrintWrit