【问题标题】:How do maps check keys? [duplicate]地图如何检查密钥? [复制]
【发布时间】:2013-10-09 12:25:18
【问题描述】:

这已经困扰了我很长一段时间了。基本上,每当我想将我自己的一组对象(我在这里称之为 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 作为实例保存在我的类中并将该实例用作get 方法的参数。

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

【问题讨论】:

  • Map 将根据实现检查键。 HashMap 使用哈希码来做。
  • @porfiriopartida 是否有另一个地图可以用来比较两个相似的对象?
  • 是的,地图文档中很少。只需谷歌搜索 java.util.Map 并查看所有已知的实现类部分。
  • @Slanec 是的,这正是我想要的。可能只是用谷歌搜索了错误的标签。

标签: java object map compare


【解决方案1】:

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

【讨论】:

    【解决方案2】:

    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 个元素,如果你的 key 对象生成一个 5,那么它会将 value 对象存储到数组[5]

    这里是get(Object key)的部分代码:

    HashMap.get(Object)

    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 哈希作为索引的对象。

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

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

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

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

    【讨论】:

      【解决方案3】:

      为了让两个对象就映射而言“相同”,它们的hashCode 方法必须返回相同的值,并且它们上的equals 方法必须在将另一个作为论据。

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

      所以当你写的时候:

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

      假设您没有在MyObject 中覆盖hashCodeequals,这将是两个具有不同哈希码且比较不相等的不同对象。

      如果您希望您的不同对象在地图方面是“相同的”(就像IntegerString 这样的类),您需要覆盖hashCodeequals 方法:

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

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

      现在您可能不希望所有MyObjects 完全相同——您可能在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; }
      

      【讨论】:

        【解决方案4】:

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

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

        【讨论】:

          【解决方案5】:

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

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

          这就是为什么你的

           may.get(myObject)
          

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

          【讨论】:

            【解决方案6】:

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

            当出现像你这样的HashMap 的问题时,说明这些方法的一般约定没有履行。摘自 Object 类的 Java 文档:

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

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

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

            hashCode的总合约是:

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

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

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

            【讨论】:

              【解决方案7】:

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

              【讨论】:

                猜你喜欢
                • 2011-04-20
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 2020-09-02
                • 1970-01-01
                • 2016-11-06
                • 2011-09-07
                • 2018-06-09
                相关资源
                最近更新 更多