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

时间:2009-11-23 08:52:02

标签: java hashmap hashcode hashset language-implementation

我一直在努力了解java.util.HashMapjava.util.HashSet的内部实施。

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

  1. 什么是HashMap / HashSet中@Override public int hashcode()的重要性?这个哈希码在内部使用在哪里?
  2. 我一般认为HashMap的关键是String,如myMap<String,Object>。我可以将someObject(而不是字符串)的值映射为myMap<someObject, Object>吗?我需要遵守的所有合同成功发生了什么?
  3. 提前致谢!

    修改

    1. 我们是说密钥的哈希码(check!)是在哈希表中映射值的实际内容吗?当我们myMap.get(someKey);时,java在内部调用someKey.hashCode()来获取哈希表中的数字以查找结果值?
    2. 答案:是。

      编辑2:

      1. java.util.HashSet中,从哪里为哈希表生成密钥?它来自我们正在添加的对象,例如。 mySet.add(myObject);然后myObject.hashCode()将决定它在哈希表中的位置? (因为我们不在HashSet中提供密钥)。
      2. 答案:添加的对象成为关键。价值是假的!

9 个答案:

答案 0 :(得分:14)

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

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

实用的旁注:eclipse(以及可能还有其他IDE)可以为你的类自动生成一对equals()和hashcode()实现,只是基于类成员。

修改

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

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

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

现在我们了解到他有一个新的电话号码,我们更改了John Doe对象的电话号码字段。这导致新的哈希码。这个哈希码解析为一个新的哈希表索引 - 通常不是John Do'最喜欢的甜甜圈商店的存储位置。

问题很明显:在这种情况下,我们想要将“John Doe”映射到Donut商店,而不是“带有特定电话号码的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;
}

答案 1 :(得分:5)

  

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

这允许地图实例根据地图的内容生成有用的哈希码。具有相同内容的两个映射将生成相同的哈希码。如果内容不同,则哈希码将不同。

  

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

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

  

我可以将someObject(而不是String)的值映射为myMap<someObject, Object>吗?

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

  

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

该课程必须实施hashCode()equals()

[编辑]

  

我们是说密钥的哈希码(check!)是在哈希表中映射值的实际内容吗?

答案 2 :(得分:5)

是。您可以使用任何对象作为HashMap中的键。为了做到这一点,您必须遵循以下步骤。

  1. 覆盖等于。

  2. 覆盖hashCode。

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

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

    这是来自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;
    }
    

    从上面的代码可以清楚地看出,每个键的hashCode不仅用于地图的hashCode(),而且还用于查找存储键以放置键,值对。这就是hashCode()与HashMap的性能相关的原因

答案 3 :(得分:5)

哈希像HashMapHashSet这样的容器可以通过将内容分成“桶”来快速访问存储在其中的元素。

例如,1, 2, 3, 4, 5, 6, 7, 8中存储的数字列表List会在内存中(概念上)显示:[1, 2, 3, 4, 5, 6, 7, 8]

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

现在想象一下,您希望从6List中找到值Set。使用列表,您必须从列表的开头开始并检查每个值,直到达到6,这将需要6个步骤。使用set,您可以找到正确的存储桶,检查该存储桶中的每个项目(在我们的示例中仅为2个),使其成为一个3步骤的过程。这种方法的价值会随着您拥有的数据越多而显着增加。

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

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

答案 4 :(得分:3)

  1. Java中的任何Object都必须具有hashCode()方法; HashMapHashSet不是例外。如果将哈希映射/集插入另一个哈希映射/集中,则使用此哈希码。
  2. 任何类类型都可以用作HashMap / HashSet中的键。这要求hashCode()方法返回相等对象的相等值,并且equals()方法是根据契约实现的(自反,传递,对称)。 Object的默认实现已经遵守这些契约,但如果您希望值相等而不是引用相等,则可能需要覆盖它们。

答案 5 :(得分:2)

在Java(以及.NET)中,equals(),hashcode()和哈希表之间存在复杂的关系。引用文档:

  

public int hashCode()

     
    

返回对象的哈希码值。支持此方法的好处是哈希表,例如java.util.Hashtable提供的哈希表。

         

hashCode的一般合约是:

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

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

  

该行

@Overrides public int hashCode()

只是告诉我们覆盖了hashCode()方法。这个ia 通常表示可以安全地将类型用作HashMap中的键。

是的,您可以在equals()中使用符合hashCode()HashMap合同的任何对象作为关键字。

答案 6 :(得分:2)

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

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

原因是有些人很难实现hashCode(),而hash()函数提供了更好的散列分布。它基本上是出于性能原因而完成的。

答案 7 :(得分:2)

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

答案 8 :(得分:0)

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

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

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

  • 对于两个不相等的物体。为了等于方法,然后为两个对象调用HashCode方法,它不应该强制它产生不同的值。