java.util.HashMap 和 HashSet 的内部实现

Posted

技术标签:

【中文标题】java.util.HashMap 和 HashSet 的内部实现【英文标题】:Internal implementation of java.util.HashMap and HashSet 【发布时间】:2010-12-19 09:45:35 【问题描述】:

我一直在尝试了解java.util.HashMapjava.util.HashSet的内部实现。

以下是我脑海中浮现的疑问:

    @Override public int hashcode() 在 HashMap/HashSet 中的重要性是什么?这个哈希码在内部使用在哪里? 我通常看到 HashMap 的键是String,如myMap<String,Object>。我可以像myMap<someObject, Object> 那样将值映射到someObject(而不是字符串)吗?我需要遵守哪些合同才能成功实现这一目标?

提前致谢!

编辑:

    我们是说键的哈希码(检查!)是哈希表中映射值的实际对象吗?而当我们 myMap.get(someKey); 时,java 是在内部调用 someKey.hashCode() 来获取哈希表中的数字以查找结果值?

回答:是的。

编辑 2:

    java.util.HashSet 中,哈希表的键是从哪里生成的?是否来自我们正在添加的对象,例如。 mySet.add(myObject); 然后myObject.hashCode() 将决定它在哈希表中的位置? (因为我们不在 HashSet 中提供键)。

答案:添加的对象成为键。该值是虚拟的!

【问题讨论】:

【参考方案1】:

问题 2 的答案很简单 - 是的,您可以使用任何您喜欢的对象。具有字符串类型键的映射被广泛使用,因为它们是命名服务的典型数据结构。但总的来说,您可以映射任意两种类型,例如 Map<Car,Vendor>Map<Student,Course>

对于 hashcode() 方法,就像之前回答的那样 - 每当您覆盖 equals() 时,您都必须覆盖 hashcode() 以遵守合同。另一方面,如果您对 equals() 的标准实现感到满意,那么您不应该接触 hashcode()(因为这可能会破坏合同并导致不相等对象的哈希码相同)。

实用的旁注:eclipse(可能还有其他 IDE)可以根据类成员为您的类自动生成一对 equals() 和 hashcode() 实现。

编辑

对于您的附加问题:是的,完全正确。查看 HashMap.get(Object key); 的源码它调用 key.hashcode 来计算内部哈希表中的位置(bin)并返回该位置的值(如果有的话)。

但要小心使用“手工制作”的 hashcode/equals 方法 - 如果您使用对象作为键,请确保 hashcode 之后不会更改,否则您将找不到映射的值。换句话说,您用于计算等号和哈希码的字段应该是最终的(或在创建对象后“不可更改”)。

假设,我们与String nameString phonenumber 有联系,我们使用这两个字段来计算equals() 和hashcode()。现在我们用他的手机号码创建“John Doe”并将他映射到他最喜欢的甜甜圈店。 hashcode() 用于计算哈希表中的索引(bin),这就是甜甜圈店的存储位置。

现在我们知道他有一个新的电话号码,我们更改了 John Doe 对象的电话号码字段。这会产生一个新的哈希码。这个哈希码解析为一个新的哈希表索引——这通常不是存储 John Does 最喜欢的甜甜圈店的位置。

问题很明确:在这种情况下,我们希望将“John Doe”映射到甜甜圈店,而不是“具有特定电话号码的 John Doe”。因此,我们必须小心自动生成的 equals/hashcode 以确保它们是我们真正想要的,因为它们可能会使用不需要的字段,从而给 HashMaps 和 HashSets 带来麻烦。

编辑 2

如果将对象添加到 HashSet,则 Object 是内部哈希表的键,值已设置但未使用(只是 Object 的静态实例)。这是来自 openjdk 6 (b17) 的实现:

// Dummy value to associate with an Object in the backing Map
private static final Object PRESENT = new Object();
private transient HashMap<E,Object> map;

public boolean add(E e) 
  return map.put(e, PRESENT)==null;

【讨论】:

"该值已设置但未使用(只是 Object 的静态实例)。"我没有完全明白这一点..请解释..其次,在 HashSet 中,如果 obj 的值在病房后发生变化.. 你提到的 HashMap 问题(键的哈希码被改变,不可追踪)不应该发生。 。 正确的?确认...【参考方案2】:

用于 HashSet、HashTable、HashMap 等集合类的 HashCode 方法 - 哈希码返回用于散列目的的对象的整数。它是通过将对象的内部地址转换为整数来实现的。哈希码方法应该在每个覆盖equals方法的类中被覆盖。 HashCode方法的三个通用联系方式

对于两个相等的对象,根据。等于方法,然后为两个对象调用 HashCode 它应该产生相同的整数值。

如果对单个对象多次调用,则应返回常量整数值。

对于两个不相等的对象,根据。等于方法,然后为两个对象调用 HashCode 方法,它应该产生不同的值不是强制性的。

【讨论】:

【参考方案3】:

HashMapHashSet 这样的散列容器通过将内容拆分为“桶”来提供对存储在其中的元素的快速访问。

例如,存储在List 中的数字列表:1, 2, 3, 4, 5, 6, 7, 8 在内存中看起来(概念上)类似于:[1, 2, 3, 4, 5, 6, 7, 8]

Set 中存储相同的一组数字看起来更像这样:[1, 2] [3, 4] [5, 6] [7, 8]。在此示例中,列表已拆分为 4 个存储桶。

现在假设您想从ListSet 中找出值6。对于列表,您必须从列表的开头开始检查每个值,直到达到 6,这将需要 6 个步骤。使用一组您可以找到正确的存储桶,检查该存储桶中的每个项目(在我们的示例中只有 2 个),这使得这是一个 3 步过程。您拥有的数据越多,这种方法的价值就会显着增加。

但是等等,我们怎么知道要查看哪个桶?这就是hashCode 方法的用武之地。要确定要在其中查找项目的存储桶,Java 散列容器调用hashCode,然后对结果应用一些函数。此函数尝试平衡桶数和项目数,以尽可能快地查找。

在查找过程中,一旦找到正确的存储桶,该存储桶中的每个项目都会像列表一样一次比较一个。这就是为什么当您覆盖hashCode 时,您还必须覆盖equals。因此,如果任何类型的对象同时具有equalshashCode 方法,则它可以用作Map 中的键或Set 中的条目。为了正确实现这些方法,必须遵守一个合同,关于此的规范文本来自 Josh Bloch 的好书 Effective Java:Item 8: Always override hashCode when you override equals

【讨论】:

非常好的解释 Tendayi..“在查找过程中,一旦找到正确的存储桶,该存储桶中的每个项目都会像列表一样一次比较一个。” ..你为什么要做这个比较..因为我们不知道对象,我们传递了密钥.. 这个解释主要是针对当你在一个Set或者Map中搜索一个item的时候。但是,当您向容器中添加项目时,您仍然需要检查现有项目。这是因为 Set 项或 Map 键只能出现一次,即添加一个等于集合中已经存在的项(根据 equals 方法的实现)会覆盖现有项。【参考方案4】:

Aaron Digulla 是绝对正确的。人们似乎没有意识到的一个有趣的附加说明是键对象的 hashCode() 方法没有逐字使用。事实上,它被 HashMap 重新散列,即它调用hash(someKey.hashCode)),其中hash() 是一个内部散列方法。

要查看此内容,请查看来源:http://kickjava.com/src/java/util/HashMap.java.htm

原因是有些人对 hashCode() 的实现很差,而 hash() 函数提供了更好的哈希分布。基本上是出于性能原因。

【讨论】:

【参考方案5】:

在回答问题 2 时,尽管您可以将任何类用作 Hashmap 中的键,但最佳做法是使用不可变类作为 HashMap 的键。或者至少如果您的“hashCode”和“equals”实现依赖于类的某些属性,那么您应该注意不要提供更改这些属性的方法。

【讨论】:

"虽然您可以拥有任何可用作 Hashmap 中的键的类,但最佳做法是使用不可变类作为 HashMap 的键"让我大开眼界.. 谢谢 Sateesh.. 【参考方案6】:

是的。您可以使用任何对象作为 HashMap 中的键。为此,您必须遵循以下步骤。

    覆盖等于。

    覆盖哈希码。

java.lang.Object 的文档中非常清楚地提到了这两种方法的约定。 http://java.sun.com/javase/6/docs/api/java/lang/Object.html

是的,HashMap 内部使用了 hashCode() 方法,因此返回正确的值对性能很重要。

这里是 HashMap 中的 hashCode() 方法

public V put(K key, V value) 
    if (key == null)
        return putForNullKey(value);
    int hash = hash(key.hashCode());
    int i = indexFor(hash, table.length);
    for (Entry<K,V> e = table[i]; e != null; e = e.next) 
        Object k;
        if (e.hash == hash && ((k = e.key) == key || key.equals(k))) 
            V oldValue = e.value;
            e.value = value;
            e.recordAccess(this);
            return oldValue;
        
    

    modCount++;
    addEntry(hash, key, value, i);
    return null;

从上面的代码可以看出,每个key的hashCode不仅用于map的hashCode(),还用于查找bucket来放置key,value对。这就是为什么 hashCode() 与 HashMap 的性能有关

【讨论】:

“每个键的 hashCode 不仅用于地图的 hashCode()”,请您澄清一下……我想……它用于决定桶..【参考方案7】:

@Override public int hashcode() 在 HashMap/HashSet 中的重要性是什么?

这允许地图实例根据地图的内容生成有用的哈希码。具有相同内容的两个地图将产生相同的哈希码。如果内容不同,hash code也会不同。

这个哈希码在内部用在哪里?

从来没有。此代码仅存在,因此您可以将地图用作另一个地图中的键。

我可以将值映射到someObject(而不是String),比如myMap&lt;someObject, Object&gt;吗?

是的,但someObject 必须是一个类,而不是一个对象(你的名字暗示你想传入对象;它应该是SomeObject 以明确你指的是类型)。

我需要遵守哪些合同才能成功完成?

该类必须实现hashCode()equals()

[编辑]

我们是说键的哈希码(检查!)是哈希表中映射值的实际对象吗?

是的。

【讨论】:

您是说地图的哈希码是根据内容计算的,这意味着它可以在地图生命周期内发生变化。稍后您编写该映射可以用作另一个映射中的键。将哈希码可以更改的对象作为哈希集合中的键是非常危险的,并且会导致内存泄漏 @Luno - 是的,但这是设计应用程序的人的责任。事实上,Set API 要求 equals 被覆盖,因此hashcode 也必须被覆盖以匹配。 @Johannes:不,这是外部使用。【参考方案8】:
    Java 中的任何Object 都必须有一个hashCode() 方法; HashMapHashSet 没有例外。如果您将哈希映射/集插入另一个哈希映射/集,则使用此哈希码。 任何类类型都可以用作HashMap/HashSet 中的键。这要求hashCode() 方法为相等的对象返回相等的值,并且equals() 方法是根据契约(自反、传递、对称)实现的。 Object 的默认实现已经遵守这些约定,但如果您想要值相等而不是引用相等,则可能需要覆盖它们。

【讨论】:

【参考方案9】:

equals()、hashcode() 和 Java 中的一般哈希表之间存在着错综复杂的关系(就此而言,.NET 也是如此)。引用文档:

public int hashCode()

返回对象的哈希码值。支持这种方法是为了便于使用 java.util.Hashtable 提供的哈希表。

hashCode 的一般合约是:

只要在 Java 应用程序执行期间对同一个对象多次调用,hashCode 方法必须始终返回相同的整数,前提是没有修改对象上的 equals 比较中使用的信息。该整数不需要从应用程序的一次执行到同一应用程序的另一次执行保持一致。 如果根据 equals(Object) 方法,两个对象相等,则对两个对象中的每一个调用 hashCode 方法必须产生相同的整数结果。 如果根据 equals(java.lang.Object) 方法,如果两个对象不相等,则不需要对两个对象中的每一个调用 hashCode 方法必须产生不同的整数结果。但是,程序员应该意识到,为不相等的对象生成不同的整数结果可能会提高哈希表的性能。

在合理可行的情况下,Object 类定义的 hashCode 方法确实为不同的对象返回不同的整数。 (这通常通过将对象的内部地址转换为整数来实现,但 Java™ 编程语言不需要这种实现技术。)

线

@Overrides public int hashCode()

只是告诉hashCode() 方法被覆盖。这 通常表明在 HashMap 中使用该类型作为键是安全的。

是的,您可以轻松地将任何遵守equals()hashCode() 合同的对象用作HashMap 中的键。

【讨论】:

"这通常表明在 HashMap 中使用类型作为键是安全的。"这完美地回答了我的问题2。非常感谢!

以上是关于java.util.HashMap 和 HashSet 的内部实现的主要内容,如果未能解决你的问题,请参考以下文章

java.util.hashMap 中的 init 方法

在java里怎么把hashmap转换成arraylist和iterator

PySpark 会话中缺少 java.util.HashMap

java.util.HashMap 不能转换为 java.util.ArrayList

LinkedHashMap和HashMap的比较使用

LinkedHashMap和HashMap的比较使用(转)