理解HashMap中equals和hashCode的工作原理

时间:2009-12-12 19:00:10

标签: java hashmap equals hashcode

我有这个测试代码:

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。为什么呢?

9 个答案:

答案 0 :(得分:61)

HashMap使用hashCode()==equals()进行条目查找。给定键k的查找序列如下:

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

为了演示使用示例,假设我们要创建一个HashMap,其中如果键具有相同的整数值(由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()equals() :默认情况下,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() HashMapkey1key2映射到同一个存储分区,但由于{{{}} key1 == key2key1.equals(key2) 1}}和equals()检查失败,因为默认情况下==使用key3检查,并且它们引用不同的实例。 ==equals()key1进行key2class 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 会为每个操作分配一个不同的存储桶,因此您永远不会找到您之前输入的相同条目。 / p>

hashCode()

如果HashMap始终相同,该怎么办?HashMap将所有密钥映射到一个大桶中。在这种情况下,您的代码在功能上是正确的,但List的使用实际上是多余的,因为任何检索都需要在O(N)时间(or O(logN) for Java 8)内迭代该单个存储桶中的所有条目,相当于使用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当我们将同一个实例与自身进行比较时检查通过,但否则失败,key1始终检查失败,因此key2key3hashCode()被视为“逻辑上不同”,并映射到不同的条目,但由于相同的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 ),相同的条目。

def Area_Of_A_Rectangle():
  print("To Find the Area of a Rectangle we need two things")
  print("The Area of a Rectangle: ",Rectangle_Height() * Rectangle_Width() )


##!--------------------Validation Modules------------------!##


#-----Rectangle Height Input------------------------------------------------    
def Rectangle_Height():
  try:
    Rec_Height = float(input("What is the Height of your Rectangle: "))
  except ValueError:
    print("Please Try Again!")
    Rectangle_Height()
  else:
    return Rec_Height

#-----Rectangle Width-------------------------------------------------------
def Rectangle_Width():
  try:
    Rec_Width = float(input("What is the Width of your Rectangle: "))
  except ValueError:
    print("Please Try Again!")
    Rectangle_Width()
  else:
    return Rec_Width


Area_Of_A_Rectangle()

答案 1 :(得分:44)

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

在哈希映射中,仅在哈希桶中测试相等性。你的两个星期一对象应该是相同的,但是因为它们返回不同的哈希码,所以甚至不调用equals方法来确定它们的相等性 - 它们被直接放入不同的桶中,并且它们相等的可能性不是甚至考虑过。

答案 2 :(得分:8)

我无法强调你应该阅读Chapter 3 in Effective Java(警告:pdf链接)。在本章中,您将了解有关Object中覆盖方法的所有信息,尤其是equals合同。 Josh Bloch有一个很好的方法来覆盖你应该遵循的equals方法。它将帮助您理解为什么在equals方法的特定实现中使用==而不是equals

希望这会有所帮助。请仔细阅读。 (至少前几个项目......然后你会想要阅读其余部分: - )。

- 汤姆

答案 3 :(得分:6)

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

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

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

public int hashCode() {
    return (day != null) ? day.hashCode() : 0;
}

答案 4 :(得分: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()时所采用的字段。 这将保证如果两个对象相等,它们将始终具有相同的hashCodes。 hashCodes不应被视为伪随机数,因为它们必须与equals()

一致

答案 5 :(得分:3)

根据Effective Java,

  

覆盖equals()

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

根据以上所述,Java关联数据结构比较equals()和hashCode()invokations获得的结果来创建存储桶。如果两者相同,则对象等于;否则没有。

在特定情况下(即上面提到的那个),当hashCode()评论时,为每个实例(Object继承的行为)生成一个随机数作为hash,equals()检查String的引用(记住Java String Pool),所以equals()应返回 true 但hashCode()不返回,结果是 3个不同的对象存储。 让我们看看在hashCode()尊重合同但总是返回9的情况下取消注释会发生什么。好吧,hashCode()总是一样的,equals()为池中的两个字符串返回 true (即“星期一”),对于它们,桶将是相同的结果只存储了2个元素

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

答案 6 :(得分:0)

当取消注释hashCode时,HashMap将t1和t2看作同一个东西;因此,t2的价值是t1的价值。要了解其工作原理,请注意,当hashCode为两个实例返回相同的内容时,它们最终会转到相同的HashMap存储桶。当您尝试将第二个内容插入同一个存储桶时(在这种情况下,当t1已经存在时插入了t2),HashMap会扫描存储桶以获取另一个等于的键。在您的情况下,t1和t2是相等的,因为它们具有相同的一天。那时,“payBills”蠢货“doLaundry”。至于t2 clobbers t1是否为关键,我相信这是不确定的;因此,任何一种行为都是允许的。

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

  1. 两个ToDos实例是否真的相同,只是因为它们在一周中的同一天?
  2. 每当实现equals时,都应该实现hashCode,以便任何两个等于的对象也具有相同的hashCode值。这是HashMap的基本假设。这可能也适用于依赖hashCode方法的任何其他内容。
  3. 设计hashCode方法,使哈希码均匀分布;否则,您将无法获得散列的性能优势。从这个角度来看,返回9是你可以做的最糟糕的事情之一。

答案 7 :(得分:0)

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

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

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

答案 8 :(得分:0)

无论何时在Java中创建新对象,JVM本身都会为其分配一个唯一的哈希码。如果你不会覆盖hashcode方法,那么object将获得唯一的hascode,因此一个唯一的存储桶(Imagine存储桶只是内存中JVM将用于查找对象的位置)。

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

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

当您评论hashcode方法时,JVM只会为所有三个对象返回不同的哈希码,因此它甚至不会使用equals方法对comapring对象感到烦恼。因此,通过hashmap实现添加Map中将有三个不同的对象。