56

我有这个测试代码:

import java.util.*;

class MapEQ {

  public static void main(String[] args) {
   Map<ToDos, String> m = new HashMap<ToDos, String>();
   ToDos t1 = new ToDos("Monday");
   ToDos t2 = new ToDos("Monday");
   ToDos t3 = new ToDos("Tuesday");
   m.put(t1, "doLaundry");
   m.put(t2, "payBills");
   m.put(t3, "cleanAttic");
   System.out.println(m.size());
} }

class ToDos{

  String day;

  ToDos(String d) { day = d; }

  public boolean equals(Object o) {
      return ((ToDos)o).day == this.day;
 }

// public int hashCode() { return 9; }
}

// public int hashCode() { return 9; }未注释 时m.size()返回 2,未注释时返回 3。为什么?

4

9 回答 9

99

HashMap使用hashCode(),==equals()用于条目查找。给定键的查找顺序k如下:

  • 用于k.hashCode()确定存储条目的存储桶(如果有)
  • 如果找到,对于该存储桶中的每个条目的键k1,如果k == k1 || k.equals(k1),则返回k1的条目
  • 任何其他结果,没有相应的条目

为了演示使用示例,假设我们要创建一个HashMapwhere 键是“逻辑上等价”的东西,如果它们具有相同的整数值,由AmbiguousInteger类表示。然后我们构造一个HashMap,放入一个条目,然后尝试覆盖它的值并按键检索值。

class AmbiguousInteger {
    private final int value;

    AmbiguousInteger(int value) {
        this.value = value;
    }
}

HashMap<AmbiguousInteger, Integer> map = new HashMap<>();
// logically equivalent keys
AmbiguousInteger key1 = new AmbiguousInteger(1),
                 key2 = new AmbiguousInteger(1),
                 key3 = new AmbiguousInteger(1);
map.put(key1, 1); // put in value for entry '1'
map.put(key2, 2); // attempt to override value for entry '1'
System.out.println(map.get(key1));
System.out.println(map.get(key2));
System.out.println(map.get(key3));

Expected: 2, 2, 2

不要覆盖hashCode()andequals() : 默认情况下,Java会hashCode()为不同的对象生成不同的值,因此HashMap使用这些值来映射key1key2进入不同的存储桶。key3没有对应的桶所以没有价值。

class AmbiguousInteger {
    private final int value;

    AmbiguousInteger(int value) {
        this.value = value;
    }
}

map.put(key1, 1); // map to bucket 1, set as entry 1[1]
map.put(key2, 2); // map to bucket 2, set as entry 2[1]
map.get(key1); // map to bucket 1, get as entry 1[1]
map.get(key2); // map to bucket 2, get as entry 2[1]
map.get(key3); // map to no bucket
Expected: 2, 2, 2
Output:   1, 2, null

仅覆盖hashCode() 将和HashMap映射到同一个存储桶中,但由于两者和检查失败,它们仍然是不同的条目,默认情况下使用检查,它们引用不同的实例。两者都失败并检查,因此没有相应的值。key1key2key1 == key2key1.equals(key2)equals()==key3==equals()key1key2

class AmbiguousInteger {
    private final int value;

    AmbiguousInteger(int value) {
        this.value = value;
    }

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

map.put(key1, 1); // map to bucket 1, set as entry 1[1]
map.put(key2, 2); // map to bucket 1, set as entry 1[2]
map.get(key1); // map to bucket 1, get as entry 1[1]
map.get(key2); // map to bucket 1, get as entry 1[2]
map.get(key3); // map to bucket 1, no corresponding entry
Expected: 2, 2, 2
Output:   1, 2, null

仅覆盖equals() HashMap将所有键映射到不同的桶中,因为默认不同hashCode()==equals()检查在这里无关紧要,因为HashMap永远不会到达需要使用它们的地步。

class AmbiguousInteger {
    private final int value;

    AmbiguousInteger(int value) {
        this.value = value;
    }

    @Override
    public boolean equals(Object obj) {
        return obj instanceof AmbiguousInteger && value == ((AmbiguousInteger) obj).value;
    }
}

map.put(key1, 1); // map to bucket 1, set as entry 1[1]
map.put(key2, 2); // map to bucket 2, set as entry 2[1]
map.get(key1); // map to bucket 1, get as entry 1[1]
map.get(key2); // map to bucket 2, get as entry 2[1]
map.get(key3); // map to no bucket
Expected: 2, 2, 2
Actual:   1, 2, null

覆盖hashCode()equals():HashMap映射key1,key2key3到同一个桶中。==比较不同实例时检查失败,但equals()检查通过,因为它们都具有相同的值,并且被我们的逻辑视为“逻辑上等效”。

class AmbiguousInteger {
    private final int value;

    AmbiguousInteger(int value) {
        this.value = value;
    }

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

    @Override
    public boolean equals(Object obj) {
        return obj instanceof AmbiguousInteger && value == ((AmbiguousInteger) obj).value;
    }
}

map.put(key1, 1); // map to bucket 1, set as entry 1[1]
map.put(key2, 2); // map to bucket 1, set as entry 1[1], override value
map.get(key1); // map to bucket 1, get as entry 1[1]
map.get(key2); // map to bucket 1, get as entry 1[1]
map.get(key3); // map to bucket 1, get as entry 1[1]
Expected: 2, 2, 2
Actual:   2, 2, 2

如果hashCode()是随机的呢?:HashMap将为每个操作分配一个不同的存储桶,因此您永远不会找到您之前放入的相同条目。

class AmbiguousInteger {
    private static int staticInt;
    private final int value;

    AmbiguousInteger(int value) {
        this.value = value;
    }

    @Override
    public int hashCode() {
        return ++staticInt; // every subsequent call gets different value
    }

    @Override
    public boolean equals(Object obj) {
        return obj instanceof AmbiguousInteger && value == ((AmbiguousInteger) obj).value;
    }
}

map.put(key1, 1); // map to bucket 1, set as entry 1[1]
map.put(key2, 2); // map to bucket 2, set as entry 2[1]
map.get(key1); // map to no bucket, no corresponding value
map.get(key2); // map to no bucket, no corresponding value
map.get(key3); // map to no bucket, no corresponding value
Expected: 2, 2, 2
Actual:   null, null, null

如果hashCode()总是一样呢?:HashMap将所有键映射到一个大桶中。在这种情况下,您的代码在功能上是正确的,但使用实际上是多余的,因为任何检索都需要在 O(N) 时间(或 Java 8 的 O(logN)HashMap)内遍历该单个存储桶中的所有条目,等效使用一个.List

class AmbiguousInteger {
    private final int value;

    AmbiguousInteger(int value) {
        this.value = value;
    }

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

    @Override
    public boolean equals(Object obj) {
        return obj instanceof AmbiguousInteger && value == ((AmbiguousInteger) obj).value;
    }
}

map.put(key1, 1); // map to bucket 1, set as entry 1[1]
map.put(key2, 2); // map to bucket 1, set as entry 1[1]
map.get(key1); // map to bucket 1, get as entry 1[1]
map.get(key2); // map to bucket 1, get as entry 1[1]
map.get(key3); // map to bucket 1, get as entry 1[1]
Expected: 2, 2, 2
Actual:   2, 2, 2

如果equals总是假的呢?:==当我们将同一个实例与其自身进行比较时,检查通过,否则失败,equals检查总是失败key1key2因此key3被视为“逻辑上不同”,并映射到不同的条目,尽管由于相同的原因它们仍在同一个桶中hashCode()

class AmbiguousInteger {
    private final int value;

    AmbiguousInteger(int value) {
        this.value = value;
    }

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

    @Override
    public boolean equals(Object obj) {
        return false;
    }
}

map.put(key1, 1); // map to bucket 1, set as entry 1[1]
map.put(key2, 2); // map to bucket 1, set as entry 1[2]
map.get(key1); // map to bucket 1, get as entry 1[1]
map.get(key2); // map to bucket 1, get as entry 1[2]
map.get(key3); // map to bucket 1, no corresponding entry
Expected: 2, 2, 2
Actual:   1, 2, null

好吧,如果equals现在总是正确的呢?:您基本上是说所有对象都被认为与另一个对象“逻辑上等效”,因此它们都映射到同一个桶(由于相同hashCode()),相同的条目。

class AmbiguousInteger {
    private final int value;

    AmbiguousInteger(int value) {
        this.value = value;
    }

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

    @Override
    public boolean equals(Object obj) {
        return true;
    }
}

map.put(key1, 1); // map to bucket 1, set as entry 1[1]
map.put(key2, 2); // map to bucket 1, set as entry 1[1], override value
map.put(new AmbiguousInteger(100), 100); // map to bucket 1, set as entry1[1], override value
map.get(key1); // map to bucket 1, get as entry 1[1]
map.get(key2); // map to bucket 1, get as entry 1[1]
map.get(key3); // map to bucket 1, get as entry 1[1]
Expected: 2, 2, 2
Actual:   100, 100, 100
于 2016-07-25T04:22:54.510 回答
49

您已经覆盖equals而没有覆盖hashCode. 您必须确保equals对于两个对象返回 true的所有情况,hashCode返回相同的值。哈希码是两个对象相等时必须相等的代码(反之不必为真)。当您输入硬编码值 9 时,您再次满足合同。

在您的哈希图中,仅在哈希桶中测试相等性。您的两个 Monday 对象应该相等,但由于它们返回不同的哈希码,因此equals甚至没有调用该方法来确定它们的相等性 - 它们被直接放入不同的存储桶中,甚至没有考虑它们相等的可能性。

于 2009-12-12T19:04:36.397 回答
8

我不能强调你应该阅读Effective Java中的第 3 章(警告:pdf 链接)。在那一章中,你将学习到关于重写方法Object,特别是关于equals合约的一切。Josh Bloch 有一个很好的方法来覆盖equals你应该遵循的方法。它将帮助你理解为什么你应该使用equals而不是==在你的方法的特定实现中equals

希望这可以帮助。请阅读。(至少前几项......然后你会想阅读其余的:-)。

-汤姆

于 2009-12-12T20:08:06.670 回答
7

当您不覆盖 hashCode() 方法时,您的 ToDos 类会从 Object 继承默认的 hashCode() 方法,这会为每个对象提供不同的哈希码。这意味着t1t2有两个不同的哈希码,即使你比较它们,它们也是相等的。根据特定的 hashmap 实现,map 可以自由地单独存储它们(实际上就是这样)。

当您正确覆盖 hashCode() 方法以确保相等的对象获得相等的哈希码时,hashmap 能够找到两个相等的对象并将它们放在同一个哈希桶中。

更好的实现会为相等的对象提供不同的哈希码,如下所示:

public int hashCode() {
    return (day != null) ? day.hashCode() : 0;
}
于 2009-12-12T19:06:30.747 回答
4

当您发表评论时,它返回 3;

因为从 Object 继承的 hashCode() 只被调用,它为 3 个 ToDos 对象返回 3 个不同的哈希码。不相等的哈希码意味着这 3 个对象注定到不同的存储桶中,并且 equals() 返回 false,因为它们是各自存储桶中的第一个进入者。如果 hashCodes 不同,则预先理解对象是不相等的。他们将进入不同的桶。

当您取消注释时,它返回 2;

因为这里调用了被覆盖的 hashCode(),它为所有的 ToDos 返回相同的值,它们都必须进入一个桶,线性连接。相等的哈希码不承诺任何关于对象的相等或不等的事情。

t3 的 hashCode() 为 9,因为它是第一个进入者,所以 equals() 为 false 并且 t3 插入到桶中 - 比如说 bucket0。

然后 t2 获得与 9 相同的 hashCode() 将用于同一个 bucket0,随后在 bucket0 中已经存在的 t3 上的 equals() 通过重写的 equal() 的定义返回 false。

现在,hashCode() 为 9 的 t1 也将发往 bucket0,并且与同一存储桶中预先存在的 t2 相比,随后的 equals() 调用返回 true。t1 进入地图失败。所以地图的净大小是 2 -> {ToDos@9=cleanAttic, ToDos@9=payBills}

这解释了同时实现 equals() 和 hashCode() 的重要性,并且在确定 hashCode() 时也必须采用在确定 equals() 时占用的字段。这将保证如果两个对象相等,它们将始终具有相同的 hashCode。hashCodes 不应被视为伪随机数,因为它们必须与 equals() 一致

于 2011-09-24T06:57:25.893 回答
4

根据有效的Java,

覆盖 equals() 时始终覆盖 hashCode()

好吧,为什么?很简单,因为不同的对象(内容,而不是引用)应该得到不同的哈希码;另一方面,相等的对象应该得到相同的哈希码。

根据上述,Java 关联数据结构比较 equals() 和 hashCode() 调用获得的结果以创建桶。如果两者相同,则对象相等;否则不是。

在特定情况下(即上面介绍的情况),当 hashCode() 被注释时,会为每个实例(由 Object 继承的行为)生成一个随机数作为哈希,equals() 检查字符串的引用(记住 Java 字符串池),所以 equals() 应该返回true但 hashCode() 不是,结果是存储了 3 个不同的对象。让我们看看如果 hashCode() 遵守合同但总是返回 9 是uncommented会发生什么。好吧,hashCode() 始终相同,equals() 对池中的两个字符串(即“星期一”)返回true,并且对它们而言,存储桶将是相同的,因此只存储了 2 个元素

因此,在使用 hashCode() 和 equals() 覆盖时绝对需要小心,特别是当复合数据类型是用户定义的并且它们与 Java 关联数据结构一起使用时。

于 2014-08-02T16:35:51.263 回答
0

与其考虑hashCode哈希桶映射,我认为更抽象地思考更有帮助:两个对象具有不同哈希码的观察构成了对象不相等的观察。因此,一个集合中没有一个对象具有特定哈希码的观察构成了一个集合中没有一个对象等于具有该哈希码的任何对象的观察。此外,观察到集合中的所有对象都没有具有某种特征的哈希码,这构成了一种观察,即它们都不等于任何具有某种特征的对象。

哈希表通常通过定义一系列特征来工作,其中一个特征将适用于每个对象的哈希码(例如“与 0 mod 47 一致”、“与 1 mod 47 一致”等),然后具有每个特征的对象集合。如果给定一个对象并且可以确定适用于它的特征,则可以知道它必须在具有该特征的事物的集合中。

哈希表通常使用一系列编号的桶是一个实现细节;重要的是,一个对象的哈希码可以快速用于识别许多它不可能相等的东西,因此不必与之比较。

于 2013-10-17T15:17:07.323 回答
0

当 hashCode 未注释时,HashMap 将 t1 和 t2 视为同一事物;因此,t2 的值比 t1 的值大。要了解它是如何工作的,请注意当 hashCode 为两个实例返回相同的东西时,它们最终会进入同一个 HashMap 存储桶。当您尝试将第二个东西插入同一个存储桶时(在这种情况下,当 t1 已经存在时插入 t2),HashMap 会扫描存储桶以寻找另一个相等的键。在您的情况下, t1 和 t2 相等,因为它们具有同一天。那时,“payBills”会破坏“doLaundry”。至于 t2 是否破坏 t1 作为 key,我相信这是未定义的;因此,任何一种行为都是允许的。

这里有一些重要的事情需要考虑:

  1. 两个 ToDos 实例真的就因为它们具有相同的星期几而相等吗?
  2. 无论何时实现 equals,都应该实现 hashCode,以便任何两个相等的对象也具有相同的 hashCode 值。这是 HashMap 所做的一个基本假设。这可能也适用于任何其他依赖 hashCode 方法的东西。
  3. 设计您的 hashCode 方法,使哈希码均匀分布;否则,您将无法获得散列的性能优势。从这个角度来看,返回 9 是你能做的最糟糕的事情之一。
于 2009-12-12T19:36:17.593 回答
0

每当您在 Java 中创建一个新对象时,JVM 都会为它分配一个唯一的哈希码。如果您不覆盖 hashcode 方法,那么 object 将获得唯一的 hascode 并因此获得唯一的存储桶(想象存储桶只不过是 JVM 将去查找对象的内存位置)。

(您可以通过在每个对象上调用 hashcode 方法并在控制台上打印它们的值来检查哈希码的唯一性)

在您取消注释哈希码方法的情况下,hashmap 首先查找具有与该方法返回的相同哈希码的存储桶。并且每次您返回相同的哈希码。现在,当 hashmap 找到该存储桶时,它将使用 euqals 方法将当前对象与驻留在存储桶中的对象进行比较。在这里它找到“星期一”,因此 hashmap 实现不允许再次添加它,因为已经有一个对象具有相同的 hashcode 和相同的 euqality 实现。

当您注释 hashcode 方法时,JVM 只是为所有三个对象返回不同的 hashcode,因此它甚至不会使用 equals 方法来处理对象。因此,Hashmap 实现会在 Map 中添加三个不同的对象。

于 2015-06-05T18:34:17.613 回答