【问题标题】:Why does the get method of HashMap have a FOR loop?为什么HashMap的get方法有FOR循环?
【发布时间】:2018-08-05 21:31:32
【问题描述】:

我正在查看 Java 7 中 HashMap 的源代码,我看到 put 方法将检查是否已经存在任何条目,如果存在则它将用新值替换旧值价值。

    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;
        }
    }

所以,基本上这意味着给定键总是只有一个条目,我也通过调试看到了这一点,但如果我错了,请纠正我。

现在,既然给定键只有一个条目,为什么get 方法有一个 FOR 循环,因为它可以直接返回值?

    for (Entry<K,V> e = table[indexFor(hash, table.length)];
         e != null;
         e = e.next) {
        Object k;
        if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
            return e.value;
    }

我觉得上面的循环是不必要的。如果我错了,请帮助我理解。

【问题讨论】:

  • 每个键一个条目,是的,但不是每个哈希一个条目。
  • wikipedia article on hash tables,尤其是关于单独链式冲突解决的部分很好地说明了值如何在链表内部存储。您需要遍历列表以达到正确的值,因为列表(存储桶)只能通过哈希码访问。
  • 您的分析不完整:您忽略了put 方法 有一个for 循环。这应该有助于回答您的问题。
  • @DanielPryden 同意。最好的方法是尝试自己实现一个 - 或者将其作为学习工具的更简单实现。迟早,您会遇到for 循环修复的完全相同的问题,您就会知道原因。

标签: java hashmap


【解决方案1】:

我认为@Eran 已经很好地回答了你的问题,@Prashant 也和其他回答过的人一起做了很好的尝试,所以让我用一个例子来解释一下,这样概念就很清楚了.

概念

基本上@Eran 想说的是,在给定的存储桶中(基本上在数组的给定索引处)可能有多个条目(只有Entry 对象),当 2 或更多的键给出不同的哈希,但给出相同的索引/存储桶位置。

现在,为了将条目放入哈希图中,这是在高层次上发生的(请仔细阅读,因为我已经加倍努力解释了一些原本不属于您问题的好东西):

  • 获取哈希:这里发生的是为给定的键计算第一个哈希(注意这不是hashCode,哈希是使用hashCode计算的,它是按原样完成的以减轻散列函数编写不佳的风险)。
  • 获取索引:这基本上是数组的索引,也就是桶的索引。现在,为什么计算这个索引而不是直接使用哈希作为索引是因为为了减轻哈希可能超过哈希映射大小的风险,所以这个索引计算步骤确保索引总是小于哈希映射的大小哈希图。

当两个键给出不同的哈希但相同的索引时,这两个键将进入同一个桶,这就是 FOR 循环很重要的原因。

示例

以下是我创建的一个简单示例,用于向您展示该概念:

public class Person {
    private int id;

    Person(int _id){
        id = _id;
    }

    public int getId() {
        return id;
    }
    public void setId(int id) {
        this.id = id;
    }

    @Override
    public int hashCode() {
        return id;
    }
}

测试类:

import java.util.Map;

public class HashMapHashingTest {
    public static void main(String[] args) {
        Person p1 = new Person(129);
        Person p2 = new Person(133);

        Map<Person, String> hashMap = new MyHashMap<>(2);
        hashMap.put(p1, "p1");
        hashMap.put(p2, "p2");
        System.out.println(hashMap);
    }
}

调试截图(请点击放大,因为它看起来很小):

注意,在上面的例子中,Person 对象给出了不同的哈希值(分别为 136 和 140)但给出了相同的索引 0,所以两个对象都在同一个桶中。在屏幕截图中,您可以看到两个对象都位于索引 0 处,并且还填充了一个 next,它基本上指向第二个对象。


更新: 另一种查看多个键进入同一个存储桶的最简单方法是创建一个类并覆盖 hashCode 方法以始终返回相同的 int 值,现在会发生什么该类的对象将提供相同的索引/存储桶位置,但由于您没有覆盖equals 方法,因此它们不会被视为相同,因此将在该索引/存储桶位置形成一个列表。

这里的另一个转折是假设您也覆盖equals 方法并比较所有对象是否相等,那么只有一个对象将出现在索引/存储桶位置,因为所有对象都是相等的。

【讨论】:

    【解决方案2】:

    我想用简单的话来表达。 put 方法有一个 FOR 循环来遍历位于同一 hashCode 桶下的键列表。

    当您将 putkey-value 对放入 hashmap 时会发生什么:

    1. 因此,对于您传递给HashMap 的每个key,它都会为其计算hashCode。
    2. 这么多keys 可以属于同一个hashCode 存储桶。现在 HashMap 将检查相同的 key 是否已经存在于同一个存储桶中。
    3. 在 Java 7 中,HashMap 将同一桶的所有键保存在一个列表中。因此,在插入密钥之前,它将遍历列表以检查是否存在相同的密钥。这就是为什么有一个 FOR 循环。

    所以在平均情况下,它的时间复杂度为:O(1),在最坏的情况下,它的时间复杂度为O(N)

    【讨论】:

      【解决方案3】:

      虽然其他答案解释了发生了什么,但 OP 对这些答案的 cmets 让我认为需要从不同的角度进行解释。

      简化示例

      假设您要将 10 个字符串折腾到哈希映射中:“A”、“B”、“C”、“Hi”、“Bye”、“Yo”、“Yo-yo”、“Z” , "1", "2"

      您正在使用HashMap 作为您的哈希映射,而不是制作您自己的哈希映射(不错的选择)。下面的一些东西不会直接使用HashMap 实现,而是会从更理论和抽象的角度来处理它。

      HashMap 并不神奇地知道您要向其中添加 10 个字符串,也不知道您稍后将放入哪些字符串。它必须提供放置你可能给它的任何东西的地方......因为它知道你将在其中放置 100,000 个字符串 - 可能是字典中的每个单词。

      假设您在创建new HashMap(n) 时选择了构造函数参数,因此您的哈希映射有 20 个。我们会打电话给他们bucket[0]bucket[19]

      1. map.put("A", value); 假设“A”的哈希值为 5。哈希映射现在可以做到bucket[5] = new Entry("A", value);

      2. map.put("B", value); 假设 hash("B") = 3。所以,bucket[3] = new Entry("B", value);

      3. map.put("C"), value); - hash("C") = 19 - bucket[19] = new Entry("C", value);

      4. map.put("Hi", value); 现在是有趣的地方。假设您的哈希函数是这样的 hash("Hi") = 3。所以现在哈希映射想要做bucket[3] = new Entry("Hi", value); 我们有一个问题! bucket[3] 是我们放置关键“ B”,并且“Hi”肯定是与“B”不同的键......但它们具有相同的哈希值。我们有一个碰撞

      由于这种可能性,HashMap 实际上并没有以这种方式实现。 哈希映射需要有可以容纳超过 1 个条目的存储桶。注意:我确实没有说多个条目使用相同的键,因为我们不能拥有那个,但它需要具有可以容纳1 个以上不同键的条目的存储桶。我们需要一个可以同时容纳“B”“Hi”的桶。

      所以我们不要使用bucket[n] = new Entry(key, value);,而是让bucket 的类型为Bucket[],而不是Entry[]。所以现在我们做bucket[n].add( new Entry(key, value) );

      所以让我们改成...

      bucket[3].add("B", value);

      bucket[3].add("Hi", value);

      如您所见,我们现在将“B”和“Hi”的条目放在同一个存储桶中。现在,当我们想要将它们取出时,我们需要遍历存储桶中的所有内容,例如,使用 for 循环

      所以由于碰撞而存在循环。 不是 key 的冲突,而是 hash(key) 的冲突。

      我们为什么要使用如此疯狂的数据结构?

      此时你可能会问,“等等,什么!?!为什么我们会做这样奇怪的事情???为什么我们要使用如此人为和复杂的数据结构???”那个问题的答案是……

      哈希映射之所以如此工作,是因为这种特殊设置为我们提供了数学运算方式所带来的属性。如果您使用了一个可以最大限度地减少冲突的良好哈希函数,并且如果您将HashMap 的大小设置为比您猜测其中包含的条目数更多的桶,那么您就有了一个优化的哈希映射这将是复杂数据的插入和查询最快的数据结构。

      你的 HashMap 可能太小了

      既然你说你经常看到这个 for 循环在你的调试中被多个元素迭代,这意味着你的 HashMap 可能太小了。如果您对可以放入多少东西有一个合理的猜测,请尝试将大小设置为更大。请注意,在上面的示例中,我插入了 10 个字符串,但有一个包含 20 个桶的哈希映射。使用良好的哈希函数,这将产生很少的冲突。

      注意:

      注意:上面的例子是对问题的简化,为了简洁起见,确实采用了一些捷径。完整的解释甚至稍微复杂一些,但在这里回答问题所需知道的一切。

      【讨论】:

        【解决方案4】:

        哈希表有桶,因为对象的哈希不必是唯一的。如果对象的哈希值相等,则意味着、对象可能相等。如果对象的哈希值不同,那么对象就完全不同。 因此,具有相同哈希的对象被分组到桶中。 for 循环用于迭代此类存储桶中包含的对象。

        实际上,这意味着在这种哈希表中查找对象的算法复杂度不是恒定的(尽管非常接近),而是介于对数和线性之间。

        【讨论】:

        • 哈希表具有恒定的复杂性,而不是对数。见问答here
        【解决方案5】:

        如果你看到 HashMap 的 get 方法的内部工作。

        public V get(Object key)  {
                if (key == null)
                   return getForNullKey();
                 int hash = hash(key.hashCode());
                 for (Entry<K,V> e = table[indexFor(hash, table.length)];e != null;e = e.next) 
                 {
                     Object k;
                     if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
                         return e.value;
                 }
                     return null;
        }
        
        • 首先,它获取传递的关键对象的哈希码,然后 找到存储桶位置。
        • 如果找到正确的桶,则返回值(e.value)
        • 如果未找到匹配项,则返回 null。

        有时可能存在 Hashcode 冲突的机会,为了解决这种冲突,Hashmap 使用 equals(),然后将该元素存储到同一桶中的 LinkedList 中。

        举个例子:

        获取键 vaibahv 的数据: map.get(new Key("vaibhav"));

        步骤:

        1. 计算Key {“vaibhav”}的哈希码,生成118。

        2. 使用索引方法计算索引为6。

        3. 转到数组的索引 6 并将第一个元素的键与给定的键进行比较 钥匙。如果两者相等则返回值,否则检查 下一个元素(如果存在)。

        4. 在我们的例子中,它不是节点对象的第一个元素和下一个元素 不为空。

        5. 如果下一个节点为空,则返回空。

        6. 如果节点的下一个不为空,则遍历第二个元素并且 重复过程3,直到找不到key或next不为null。

        对于这个检索过程,将使用 for 循环。 如需更多参考,您可以参考 this

        【讨论】:

        • 请不要将 JPG 用于非摄影图像。
        • 你能举一个例子,一个给定的键可能有多个条目吗?
        • @pjj 不是同一个key,而是同一个hash(或者同一个数组索引,即使不是同一个hash)。这里答案的重点是HashMap 没有(不能)神奇地包含一个独特的位置来放置将来将是put 的每个条目。 putHashMap 的键可以具有 hash-conflicts,即使它们不是真正相同的键,它们也希望放在结构中的相同位置。当条目是put 进入地图时,有一种特殊的方法可以解决这个问题。在get,你必须考虑到这一点。
        • @pjj 考虑hascode与不同键发生冲突的情况
        • @pjj 在这种情况下,第二个键值对将转到已经存在其他键值对的同一个存储桶。但是您正在谈论的情况将旧条目替换为新条目会发生当两个键相同时。如果键不同并且发生哈希码冲突,则两个条目将进入同一个桶。那时哈希图将保留链接列表以跟踪两个条目
        【解决方案6】:

        作为记录,在 java-8 中,这也存在(有点,因为也有 TreeNodes):

        if ((e = first.next) != null) {
                    if (first instanceof TreeNode)
                        return ((TreeNode<K,V>)first).getTreeNode(hash, key);
                    do {
                        if (e.hash == hash &&
                            ((k = e.key) == key || (key != null && key.equals(k))))
                            return e;
                    } while ((e = e.next) != null);
                }
        

        基本上(对于 bin 不是 Tree 的情况),迭代整个 bin,直到找到我们正在寻找的条目。

        看看这个实现,您可能会理解为什么提供一个好的散列是好的 - 这样并不是所有的条目最终都在同一个存储桶中,因此需要更多的时间来搜索它。

        【讨论】:

          【解决方案7】:

          table[indexFor(hash, table.length)]HashMap 的存储桶,它可能包含我们正在寻找的密钥(如果它存在于Map 中)。

          但是,每个存储桶可能包含多个条目(具有相同 hashCode() 的不同键,或者具有不同 hashCode() 的不同键仍然映射到同一个存储桶),因此您必须遍历这些条目,直到找到您正在寻找的钥匙。

          由于每个桶中的预期条目数应该非常少,因此该循环仍会在预期的O(1)时间执行。

          【讨论】:

          • 我的观点是,在put 方法中,get 的逻辑哈希和索引逻辑已经完成,最后旧值被替换为新值,如果不是一个新条目添加然后所有这些都是有道理的,但因为它没有,所以我觉得get 中的 FOR 循环真的没有意义。我同意循环会给O(1)
          • @pjj 对于给定的键,该桶中最多可以有一个条目,但同一桶中可能存在具有不同键的条目。因此,循环是必需的,直到您找到与您正在寻找的密钥相等的密钥。
          • @pjj 不同的key可以有相同的hashcode值,因此最终在同一个bucket中(bucket是根据key的hash来选择的)
          • @pjj 基本上你所说的情况是哈希码与设置的不同键发生冲突
          • @pjj 我从来没有说过给定键可以有多个条目。我说过一个给定的存储桶可以有多个条目。
          猜你喜欢
          • 1970-01-01
          • 2020-08-12
          • 2023-02-22
          • 2011-07-28
          • 1970-01-01
          • 2022-01-15
          • 2021-10-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多