为什么在Hashmap中没有调用equals方法?

时间:2014-07-02 02:39:28

标签: java hashmap

import java.util.*;
class Dog {
    public Dog(String n) { name = n; }
    public String name;
    public boolean equals(Object o) {
        System.out.println("equals called..");
        if((o instanceof Dog) &&
                (((Dog)o).name == name)) {
            return true;
        } else {
            return false;
        }
    }
    public int hashCode() {
        System.out.println("hashCodecalled..");
        return name.length(); 
    }
}
class SampleClass {
    public static void main(String[] args) {
        Map<Object, String> m = new HashMap<Object, String>();
        Dog d1 = new Dog("clover");
        m.put(d1, "Dog key");
        d1.name ="arthur";
        System.out.println(m.get(d1));      
    }
}

在上面的代码中,我得到的输出是:

hashCodecalled..
hashCodecalled..
Dog key

在我做d1.name =“arthur”之后

我期待输出

hashCodecalled..``
hashCodecalled..
equals called..
null

8 个答案:

答案 0 :(得分:6)

由于HashMap具有您正在寻找的确切对象,因此无需调用equals来验证该对象是否正确。

get来自HashMap的对象时,首先评估hashCode以找到正确的存储桶。然后,搜索存储桶。但是首先使用==比较存储桶中的每个密钥。如果对象不是正在寻找的对象,则使用equals

在Java 7中,get HashMap方法中代码的关键部分是

if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
    return e.value;

答案 1 :(得分:2)

This question has been asked before in here.

根据hashCode方法,它表示&#34;如果两个对象根据equals(Object)方法相等,则在两个对象中的每一个上调用hashCode方法必须产生相同的整数结果。&#34 ;通常,每当您创建equals方法时,请确保它与返回的哈希码值保持一致。

HashMap将始终在equal方法之前使用hashCode比较对象。对于您的情况,如果您希望看到要调用的相等方法,则可以通过仅将哈希码值设为常量来模拟哈希冲突场景。 (警告:这不是一个糟糕的例子,因为它在性能方面影响很大。)

/**
 * @param args the command line arguments
 */
public static void main(String[] args) {
    Map<Object, String> m = new HashMap<Object, String>();
    Dog d1 = new Dog("clover");
    Dog d2 = new Dog("clover 2");
    m.put(d1, "Dog 1");
    m.put(d2, "Dog 2");
    System.out.println(m.get(d1)); 
}

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

@Override
public boolean equals(final Object o) {
    System.out.println("equals called..");
    return true;
}

@Override
public int hashCode() {
    System.out.println("hashCodecalled..");
    return 1; // constant
}}

结果:

hashCodecalled ..

hashCodecalled ..

等于叫..

hashCodecalled ..

狗2

最后,当然你需要插入至少两个对象才能使它有效。

答案 2 :(得分:1)

请参阅java.util.HashMap.get方法代码。

首先检查键== e.key然后检查key.equals(e.key)。

在您的代码中,您将相同的实例传递给get方法。 所以关键== e.key。和key.equals(e.key)将不会被执行。

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

答案 3 :(得分:1)

从HashMap的代码中可以看出,它首先检查null,因为hashcode没有改变(name的长度没有改变),然后==被比较(如David所说)。这是真的,等于()不会检查,所以它会返回那个狗。

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

答案 4 :(得分:1)

David Wallace提供了简单的答案

我正在解释HashMap的工作细节,所以它可能会帮助其他人。

在HashMap中,对象存储在称为桶(密钥)的组中。具有相同hashCode值的所有关键对象都存储在一个存储桶中。因此,例如Dog k1 = new Dog("clover")Dog k2 = new Dog("clever")Dog k3 = new Dog("123456");都存储在一个存储桶中,因为hashCode() Dog方法返回的名称长度为hashCode值。这里所有3个Dog对象的名称长度为6,因此进入一个桶中。

当调用get()方法时,HashMap使用给定键的相同hashCode定位存储桶,如果找到,则在存储桶中的每个对象上首先使用simple ==相等检查来查看如果给定的桶中的密钥和对象是相同的对象,如果没有,则equals()方法相等性检查以找到确切的对象。

因此,如果在使用键(Dog对象)将键值对存储(放入)HashMap后更改了键对象内容,则可能无法查找/检索原始对象(有时可能)。让我们看看如何。

场景1:

例如,如果Dog k3 name更改为“1234”,则hashCode现在变为4.假设只有k1,k2和k3被放入HashMap,{{1}调用(现在名称为“1234”)将导致查找包含hashCode 4对象的存储桶。由于只有一个存储桶包含hashCodes为6且没有hashCode为4的存储桶,因此无法存储桶找到并且不会尝试使用equals()方法。

当我用家里的地址搜索“你”时,如果我找不到你的房子,那么就没有比较家庭成员姓名的问题了。

这里的get()方法返回null。这是无法检索的损坏条目的情况,除非再次更改密钥内容以将其hashCode值恢复为6。

场景2:

但如果get(k3) k3更改为长度为6的“654321”,则会找到存储桶并调用equals()以与放入的3个对象进行比较对于一个相同k3的对象,存储桶以及equals()都返回true。记住这里用来放置的同一个对象k3被改变了,所以改变也反映在HashMap中,它是同一个对象。这里==等式检查成功为k3并且没有调用equals()方法(这一点是从David Wallace的答案中获得的,在这一点上任何投票都请给他不是我!)。这里get()方法的结果将是期望的非null对象。这是因为密钥内容的更改保留了hashCode值为6。

场景3:

现在,如果我们单独name,又将Dog k4 = new Dog("123456");更改为k3.name。猜猜调用"654321"时会发生什么!由于给定的键是一个单独的对象,因此将定位存储桶并且==失败,然后在地图存储桶中的所有3个对象上调用equals但是等于失败并且get(k4)的最终结果为get(k4)。因为对象k3.name更改会影响存储在地图存储桶中的密钥,以便从“123456”更改为“654321”。当被要求找到“123456”时,没有键匹配“123456”,虽然最初使用该值,但它在地图中现在是“654321”。

因此,在用于存储在地图中之后更改关键对象内容是不安全的。这同样适用于HashSet也使用相同散列的Set实现。事实上,HashSet在场景后面使用了HashMap。

为什么它会像那样

为了更好的表现。如果没有使用bucketing,则每次输入新条目时都必须与所有现有(先前放置)对象进行比较以确保唯一性。此外,在检索现有条目(调用get())时,它必须与HashMap中的所有对象进行比较,直到找到匹配项。使用上面的bucketing,它可以安全地忽略一些条目/对象,以进行相等性比较。

为了支持Hash实现中使用的这种机制,在为可以用于基于Hash存储的对象实现这些方法时,应该遵循falsehashCode()方法之间的规范/契约。实现。 JavaDoc here,请阅读。

PS:我在David Wallace回答之前从等号()部分拿起了==检查并编辑到这里。谢谢大卫的评论。

答案 5 :(得分:0)

此处的问题是,hashCode基于name String的长度,而且您使用的值均为6。

HashMap还有一个优化,它在使用equals()之前执行引用相等。在这里,它不需要使用equals,因为它是相同的关键对象。

答案 6 :(得分:0)

如果您将对象用作哈希键,则应将其设为immutable。正如您所见,更改用作哈希键的对象会导致意外行为,并且不可变意味着您无法在创建对象后对其进行更改。

答案 7 :(得分:0)

您必须使用使用不可变类(例如String或包装类)作为Map的键,否则Map的工作流程(由于其散列算法)将存在不一致。

您可以将自定义类(例如Dog)设为不可变,以便稍后您无法更改已使用的密钥。

就像你用一把钥匙锁上一扇门,后来你用一些“工具”改变了同一把钥匙,然后期望打开锁着的门。