2

这已经困扰了我很长一段时间了。基本上,每当我想将我自己的一组对象(我在这里称之为 MyObject)存储为地图中的键时,我无法获得键值,除非我在我的类中保存了相同的确切对象。即使我尝试覆盖 MyObject 中的 equals 方法,在比较具有相同值的 2 个对象时它通常返回 true,但没有任何改变。

只是为了给您演示一下我的意思:


Map<Integer, Integer> map = new HashMap<Integer, Integer>();
map.put(2, 3);
System.out.println(map.get(2)));

现在,正如您可能期望的那样,它在映射中搜索整数对象 2,然后打印出 3。如果整数不存在,则打印 null。到目前为止,一切都很好。


Map<String, Integer> map = new HashMap<String, Integer>();
map.put(new String("hi"), 3);
System.out.println(map.get(new String("hi")));

这也可以按预期工作。我们只是得到键“hi”的值。


Map<MyObject, Integer> map = new HashMap<MyObject, Integer>();
map.put(new MyObject(), 3);
System.out.println(map.get(new MyObject()));

即使“new MyObject()”和“new MyObject()”在技术上没有区别,它仍然返回 null,除非我将 new MyObject 作为实例保存在我的类中并将该实例用作获取方法。

与我的 MyObject 不同,如果键是字符串或整数,则映射很容易获取键值。这些类型只是特权还是有办法告诉地图:“嘿,新创建的对象与该列表中的对象相似”?地图如何比较对象?

4

7 回答 7

4

就映射而言,为了使两个对象“相同”,它们的hashCode方法必须返回相同的值,并且equals当将另一个对象作为参数传递时,它们上的方法必须返回 true。

所有对象继承的默认值Object.hashCodeObject.equals方法都使用对象标识,因此两个不同的对象是不同的,即使它们的所有字段都相同。

所以当你写:

map.put(new MyObject(), 3);
System.out.println(map.get(new MyObject()));

假设您没有覆盖hashCodeequalsin MyObject,这些将是两个不同的对象,它们具有不同的哈希码,比较不相等。

如果您希望您的不同对象就地图而言是“相同的”(就像类一样IntegerString做的那样),您需要覆盖hashCodeandequals方法:

class MyObject {
public int hashCode() { return 42; }
public boolean equals(Object o) { return o instaceof MyObject; }
};

就 a 而言,这将使所有MyObject对象成为同一个对象Map,并且您的代码将打印3.

现在,您可能不希望 ALLMyObjects完全相同——您可能有一些字段,MyObject并且您希望仅当字段匹配时才将它们视为相同。如果是哪种情况,您可能想要类似的东西:

class MyObject {
Object field1;
Object field2;
int field3;
public int hashCode() { return field1.hashCode() + field2.hashCode() + field3; }
public boolean equals(Object o) {
    if (!(o instanceof MyObject)) return false;
    MyObject a = (MyObject)o;
    return field1.equals(a.field1) && field2.equals(a.field2) && field3 == a.field3; }
于 2013-10-02T23:38:11.010 回答
3

为了使任何启用哈希的数据结构(如HashMap, HashSet)正常工作,除了方法之外,它的元素或键必须覆盖 。原因是哈希码用于标识放置元素或键(在插入期间)或搜索(在查找期间使用)的桶。hashCode()equals()equals()

如果您不 override hashCode(),则使用默认实现 from Object#hashCode(),即使对于您认为等效的对象(该equals()方法返回true)也会返回不同的值。

这就是为什么你的

 may.get(myObject)

myObject尽管已经存在,但呼叫失败。由于哈希码不匹配,因此HashMap永远不会在正确的存储桶中查找密钥。因此,您equals()永远不会在这里被调用。

于 2013-10-02T23:39:47.083 回答
1

要在 Java 中用作Map实例的键,类必须实现一致的hashCode()equals()。如果 中没有这些方法的实现MyObject,JVM 将使用 中的实现Object,其中两个实例不相等。

于 2013-10-02T23:38:01.443 回答
0

与我的 MyObject 不同,如果键是字符串或整数,则映射很容易获取键值。这些类型只是特权还是有办法告诉地图:“嘿,新创建的对象与该列表中的对象相似”?地图如何比较对象?

TreeMap在 JAVA 中查看。TreeMap 允许我们在创建期间指定一个可选的 Comparator 对象。键应与指定的比较器兼容。这个比较器决定了键需要排序的顺序。TreeMap 比 hashmap 慢。

于 2013-10-02T23:41:02.707 回答
0

Map实现使用hashCode()对象中的方法来确定在调用时在其内部数据结构中查找的键get。在您的具体示例中,假设您的MyObject类有一个id属性:

public class MyObject {
    private int id;

    public MyObject(int id) {
        this.id = id;
    }

    public int getId() {
        return id;
    }
} 

假设您希望id在地图中查找键时使用该属性 - 无论对象的实例如何 - 您将覆盖hashCode类中的方法来执行此操作:

public class MyObject {
    private int id;

    public MyObject(int id) {
        this.id = id;
    }

    public int getId() {
        return id;
    }

    /**
     * Uses the Jakarta commons-lang HashCodeBuilder class to generate the hash code.
     *
     * @see http://commons.apache.org/proper/commons-lang/apidocs/org/apache/commons/lang3/builder
     * /HashCodeBuilder.html
     */


    public int hashCode() {
        return new HashCodeBuilder(1, 5)
                .append(id)
                .toHashCode();
    }

    public boolean equals(Object other) {
        if (other == null)
           return false;

        if (other == this) 
           return true;

        return (other.id == this.id);
    }
}

HashCodeBuilder的文档解释了作为参数传递给 HashCodeBuilder 的数字 1 和 5 的作用 - 我随机选择它们:文档说它们必须是唯一的、随机的、奇数。

于 2013-10-02T23:51:01.187 回答
0

Map 是一个接口,它将根据实现检查键。

java.util.Map

All Known Implementing Classes:
  AbstractMap, Attributes, AuthProvider, ConcurrentHashMap, ConcurrentSkipListMap, 
  EnumMap, HashMap, Hashtable, IdentityHashMap, LinkedHashMap, PrinterStateReasons, Properties, 
  Provider, RenderingHints, SimpleBindings, TabularDataSupport, TreeMap, UIDefaults, WeakHashMap

HashMap 是您在那里使用的实现,它将使用 HashTable ,当您将对象作为键插入时,它将获取其哈希码(这是一个整数)并将所需的对象放在该位置,想象这是一个 256 个元素的数组,如果你的键对象生成一个5,那么它会将值对象存储到数组[5]

下面是get(Object key)的部分代码:

HashMap.get(对象)

314     public V get(Object key) {
315         if (key == null)
316             return getForNullKey();
317         int hash = hash(key.hashCode());
318         for (Entry<K,V> e = table[indexFor(hash, table.length)];
319              e != null;
320              e = e.next) {
321             Object k;
322             if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
323                 return e.value;
324         }
325         return null;
326     }

如您所见,空键有一个特殊插槽,hashCode 属于 Object,因此所有对象都可以具有该特定整数,然后 if 将获得具有作为索引的对象作为索引产生的哈希,indesOf hash..

现在的问题是,当 2 个不同的对象产生相同的 hashCode 时会发生什么?

好吧,在 get 中有一个循环,hashCode 在对象之间不是唯一的,它将遍历具有相同 hashcode 的所有对象......所以如果我们有 10 个对象,其中 5 个具有相同的 hashcode 并且所有它们存储为键,一旦您尝试获取重复的哈希码对象之一,它将返回 5 个对象,然后它将使用 equals 来确定哪个是正确的。

TreeMap 会做类似的事情,但不是使用 hashCode,而是使用 compareTo 的整数。它在内部使用红黑树。

和那两个一样,有很多方法可以实现 Map 类,只要你符合接口的约定。

于 2013-10-02T23:37:39.463 回答
0

JavaHashMap使用hashCodeandequals方法来完成它的脏活。不同类型的地图使用不同的方式,例如 TreeMap 使用equalscompareTo因为它是一个排序的地图。

当一个HashMaplike yours出现问题时,这意味着这些方法的一般合同没有履行。类的 Java 文档的摘录Object

equals方法在非空对象引用上实现等价关系:

  • 它是自反的:对于任何非空引用值 x,x.equals(x)都应该返回 true。
  • 它是对称的:对于任何非空引用值 x 和 y,x.equals(y)当且仅当 y.equals(x) 返回 true 时才应返回 true。
  • 它是可传递的:对于任何非空引用值 x、y 和 z,如果x.equals(y)返回 true 并y.equals(z)返回 true,则 x.equals(z) 应该返回 true。
  • 它是一致的:对于任何非空引用值 x 和 y,x.equals(y)只要没有修改对象上的 equals 比较中使用的信息,多次调用一致返回 true 或一致返回 false。
  • 对于任何非空引用值 x,x.equals(null)应返回 false。

请注意,每当重写该方法时,通常都需要重写 hashCode 方法,以维护 hashCode 方法的一般约定,即相等的对象必须具有相等的哈希码。

的总合同hashCode是:

  • 如果根据方法两个对象相等equals(Object),则对两个对象中的每一个调用 hashCode 方法必须产生相同的整数结果。
  • 不要求如果两个对象根据equals(java.lang.Object)方法不相等,则对两个对象中的每一个调用 hashCode 方法必须产生不同的整数结果。但是,程序员应该意识到,为不相等的对象生成不同的整数结果可能会提高哈希表的性能。

请注意,如果不提供该hashCode方法的一致实现,如果实现了 equals ,则会发生两个元素被认为相等但其哈希码不同的情况,因此会发生奇怪的事情(如您的示例中)。

现在,如果HashMap您的自定义对象不能按预期工作,那么 99.99% 您不尊重这些合同条目中的一个或多个。为具有组合的对象提供一致的哈希码并不是那么简单,但有许多简单的解决方案就足够了。

于 2013-10-02T23:38:46.097 回答