重写hashCode()和equals()以在hashMap中存储对象未能正常工作

4

我在一个类(Dog)中重写了hashCode()和equals()方法,以便从HashMap中存储和检索它的实例,代码如下:

class Dog {

public Dog(String n) {
    name = n;
}
public String name;

public boolean equals(Object o) {
    if ((o instanceof Dog)
            && (((Dog) o).name == name)) {
       return true;
    } else {
       return false;
    }
}

public int hashCode() {
    return name.length();
   }
}

而 hashMap 的代码如下:

public class MapTest {

public static void main(String[] args) {
    Map<Object, Object> m = new HashMap<Object, Object>();
    m.put("k1", new Dog("aiko"));
    Dog d1 = new Dog("clover");
    m.put(d1, "Dog key");    // #1
    System.out.println(m.get("k1"));
    String k2 = "k2";
    d1.name = "arthur";     // #2
    System.out.println(m.get(d1)); #3 
    System.out.println(m.size()); 
  }
}

问题在于,在第2步,我更改了存储在哈希映射表1中的dog对象的名称。在第3步,期望输出为NULL,但实际输出为Dog Key!! 我预计它会在equals()方法中失败,因为clover!=arthur,但它成功了!我注意到当hashCode成功(即长度==6)时,即使equals()方法失败,仍然检索到映射中存储的值。我改变了==并使用了equals(),但问题仍然存在。

我真的希望Java编译器在x == y时发出警告/错误,其中x/y的类型不是原始类型。(顺便说一句,在C#/VS中,它确实会对各种形式进行警告。)也就是说,我真的希望需要一个明确的(object)x == (object)y形式(或其他形式)..这个问题/问题经常出现。 </rant> - user166390
1
不太明白你在做什么。如果你修改了映射中的对象,但保持键不变,那么你仍然可以使用相同的键实例获取值。使用你的代码dog.equals(dog)会始终为真(除非有并发修改)。 - Tom Hawtin - tackline
@TomHawtin-tackline 请以回答的形式发布.. 我错过了,还有其他两个发帖者也错过了。 :) - user166390
@pst 我感到困惑。你发帖,我会点赞。 - Tom Hawtin - tackline
@TomHawtin-tackline,这里为您引用评论的帖子。 - user166390
3个回答

5
您需要使用.equals()方法比较字符串,而不是使用==运算符,因为后者比较的是引用。
public boolean equals(Object o) {
    if ((o instanceof Dog)
            && (((Dog) o).name.equals(name))) {
       return true;
    } else {
       return false;
    }
}

另外,那个等于方法有点问题。如果名称为null呢?你会得到一个空指针异常。你需要再添加一个检查来处理这种特殊情况。


1
这似乎与问题无关 - 首先,所有字符串都被内部化了。 - Tom Hawtin - tackline
@Tom,你怎么知道所有的字符串都是被interned的? - Steve Kuo
1
这是一个错误的假设,因为他的源代码只是示例代码。一旦从数据库中读取字符串值,== 就会失效。使用 equals 是比较字符串的正确方式。 - Steve Kuo
1
@SteveKuo 字符串字面量被语言规范保证是内部化的。是的,您可以更改源代码并使其执行不同的操作,但问题是关于这个确切源代码的行为。 - Tom Hawtin - tackline
1
@SteveKuo 字符串字面值。在JLS中应该不难找到。 - Tom Hawtin - tackline
显示剩余3条评论

3

为什么equals“永远不会失败”?

根据Tom的评论:

...如果您修改映射中的对象但保持键不变,则仍将能够使用相同的键实例获取值。dog.equals(dog)在您的代码中始终为true(除非存在并发修改)

也就是说,这行代码:

d1.name = "arthur"; 

在HashMap中,是否已经对对象进行了变异。与之相比较(其中t“打印”true或false):

Dog d1 = new Dog("clover");
// what "put" is effectively doing: there is *no* Copy/Clone
Dog inMap = d1;
t(inMap == d1);                 // true: same object, reference equality!
d1.name = "arthur";
t(inMap.name.equals("arthur")); // true: same object! "name" member was *mutated*
t(d1.equals(inMap));            // true: same object!

因为equals方法比较的是对象本身,所以它永远不会失败 :)
我也一开始没注意到这点:要记住Java采用了按对象共享调用(Call By Object-Sharing)的语义,也就是说,传递给方法的对象没有隐式的复制/克隆/复制。
那么,如何使其失败呢?
Dog d1 = new Dog("clover");
Dog d2 = new Dog("clover");
t(d1 == d2);                   // false: different objects!
m.put(d1, "Dog key");          // put in with D1 object
System.out.println(m.get(d1)); // "Dog key"   -okay, equals
System.out.println(m.get(d2)); // "Dog key"   -okay, equals
d2.name = "arthur";            // *mutate* D2 object
t(d1.equals(d2));              // false: no longer equal
System.out.println(m.get(d1)); // "Dog key"   -okay, always equals, as per above
System.out.println(m.get(d2)); // ""          -no good, no longer equals

hashCode 是如何起作用的?

哈希码被用来确定要将键(和值)放入哪个哈希表中。在执行查找(或设置)时,首先通过哈希码查找桶,然后检查已经映射到桶中的每个键是否与equals相等。如果桶中没有键,则永远不会调用equals。

这就解释了为什么在原始帖子中将name更改为长度为8的字符串会导致查找失败:最初选择了一个不同的桶(比如一个空桶),因此不会对已经存在于其他桶中的现有键调用equals。同一个对象键可能已经存在,但它从未被查看!

那么,如何使其在具有不同哈希码时失败:

Dog d1 = new Dog("clover");
m.put(d1, "Dog key");          // put in with D1 object, hashCode = 6
System.out.println(m.get(d1)); // "Dog key" -okay, hashCode = 6, equals
d1.name = "Magnolia";          // change value such that it changes hash code
System.out.println(m.get(d1)); // ""        -fail, hashCode = 8, equals
// ^-- attaching a debugger will show d1.equals is not called

因此,要在哈希表(例如HashMap)中找到一个键,必须满足以下条件:
k.hashCode() == inMap.hashCode() && k.equals(inMap);

可能有很多哈希码映射到同一个桶。然而以上内容是确保查找能够成功的唯一保证。


当然,对于一般的字符串比较,可以参考其他回复中的正确方法。


如果我的答案正确,为什么当我传递一个超过6个字符的名称(例如d1.name="Magnolia")时它返回NULL? - Eslam Hamdy
@Eslam根据帖子中的信息找不到正确的桶。 hashCode()首先用于查找桶,仅在找到的对象上使用equals。(这就是为什么hashCode应该是稳定的原因。) - user166390
@Eslam 每个 HashMap 条目在放入映射中时都包含哈希值的副本。即使发生哈希冲突,在尝试查看键相等性之前,也会比较哈希的完整 32 位值。 - Tom Hawtin - tackline

1
除了使用==比较字符串的问题外,发生的情况是您更改了狗的名称。
Dog d1 = new Dog("clover");
m.put(d1, "Dog key");    // #1
System.out.println(m.get("k1"));
String k2 = "k2";
d1.name = "arthur";     // #2

如果你将名称更改为与原名称长度不同的名称,那么它通常会在不同的哈希桶中查找,然后找不到这只狗。

因此,在映射中进行查找时,它会在相同的哈希桶中查找,当然会找到这只狗。


这不是真的。好吧,它确实是真的。但它并不完整。OP解释了长度和桶匹配。这仍然无法解释“等于不失败”。 - user166390
肯定会查看相同的哈希桶,这是第一步,然后equals()方法在第二步中介入以确定两者是否相等,实际上它们不相等。 - Eslam Hamdy
@Eslam 但它们确实是相等的(这就是为什么equals总是返回true),请参见Tom在主帖中的评论.. - user166390
@Eslam d1 是地图中的键,而您要查找的对象是 相同的 对象。 - Daniel Fischer

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接