【问题标题】:Why not allow an external interface to provide hashCode/equals for a HashMap?为什么不允许外部接口为 HashMap 提供 hashCode/equals?
【发布时间】:2010-09-17 20:48:21
【问题描述】:

对于TreeMap,提供自定义Comparator 是微不足道的,从而覆盖添加到地图的Comparable 对象提供的语义。 HashMaps 但是不能以这种方式控制;提供散列值和相等性检查的函数不能被“侧载”。

我怀疑设计一个界面并将其改造成HashMap(或一个新类)既简单又有用?像这样的东西,除了更好的名字:

  interface Hasharator<T> {
    int alternativeHashCode(T t);
    boolean alternativeEquals(T t1, T t2);
  }

  class HasharatorMap<K, V> {
    HasharatorMap(Hasharator<? super K> hasharator) { ... }
  }

  class HasharatorSet<T> {
    HasharatorSet(Hasharator<? super T> hasharator) { ... }
  }

case insensitive Map 问题得到了一个简单的解决方案:

 new HasharatorMap(String.CASE_INSENSITIVE_EQUALITY);

这是否可行,或者您是否发现这种方法存在任何基本问题?

是否在任何现有(非 JRE)库中使用了该方法? (试过谷歌,没有运气。)

编辑:hazzen 提出了很好的解决方法,但恐怕这是我试图避免的解决方法...... ;)

编辑:将标题更改为不再提及“比较器”;我怀疑这有点令人困惑。

编辑:接受与性能相关的答案;希望得到更具体的答案!

编辑:有一个实现;请参阅下面接受的答案。

编辑:改写第一句以更清楚地表明这是我所追求的侧载(而不是排序;排序不属于 HashMap)。

【问题讨论】:

  • "这个类不保证地图的顺序;特别是,它不保证顺序会随着时间的推移保持不变。" -- HashMap 的 Javadocs。换句话说,HashMap 是无序的。
  • 此语句允许使用任何 hashCode 实现,还允许 Map 随时调整自身大小。所以在这种情况下这是一个特性而不是一个问题?

标签: java collections hashmap trove4j


【解决方案1】:

.NET 通过 IEqualityComparer(用于可以比较两个对象的类型)和 IEquatable(用于可以将自身与另一个实例进行比较的类型)来实现这一点。

事实上,我认为在 java.lang.Object 或 System.Object 中定义相等和哈希码是一个错误。尤其是平等很难以一种对继承有意义的方式来定义。我一直想写关于这个的博客……

但是是的,基本上这个想法是合理的。

【讨论】:

  • 它解释了对于给定类型可能存在多个相等概念的概念。
【解决方案2】:

对您来说有点晚了,但对于未来的访问者来说,可能值得知道 commons-collections 有一个 AbstractHashedMap(在 3.2.2 和泛型在 4.0)。你可以重写这些受保护的方法来实现你想要的行为:

protected int hash(Object key) { ... }
protected boolean isEqualKey(Object key1, Object key2) { ... }
protected boolean isEqualValue(Object value1, Object value2) { ... }
protected HashEntry createEntry(
    HashEntry next, int hashCode, Object key, Object value) { ... }

这种替代HashedMap 的示例实现是commons-collections 自己的IdentityMap(仅限于3.2.2,因为Java 从1.4 开始就有its own)。

这不如为Map 实例提供外部“Hasharator”强大。您必须为每个散列策略(组合与继承反击......)实现一个新的映射类。但还是很高兴知道。

【讨论】:

  • PlusOne。您可能希望将该链接更新为 AbstractHashedMap 以指向最终具有泛型的 v4。
【解决方案3】:

HashingStrategy 是您正在寻找的概念。它是一个策略接口,允许您定义 equals 和 hashcode 的自定义实现。

public interface HashingStrategy<E>
{
    int computeHashCode(E object);
    boolean equals(E object1, E object2);
}

您不能将HashingStrategy 与内置HashSetHashMap 一起使用。 GS Collections 包括一个名为 UnifiedSetWithHashingStrategy 的 java.util.Set 和一个名为 UnifiedMapWithHashingStrategy 的 java.util.Map。

让我们看一个例子。

public class Data
{
    private final int id;

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

    public int getId()
    {
        return id;
    }

    // No equals or hashcode
}

您可以通过以下方式设置 UnifiedSetWithHashingStrategy 并使用它。

java.util.Set<Data> set =
  new UnifiedSetWithHashingStrategy<>(HashingStrategies.fromFunction(Data::getId));
Assert.assertTrue(set.add(new Data(1)));

// contains returns true even without hashcode and equals
Assert.assertTrue(set.contains(new Data(1)));

// Second call to add() doesn't do anything and returns false
Assert.assertFalse(set.add(new Data(1)));

为什么不直接使用MapUnifiedSetWithHashingStrategy 使用的内存是UnifiedMap 的一半,是HashMap 的四分之一。有时您没有方便的键,必须创建一个合成键,例如元组。这会浪费更多内存。

我们如何执行查找?请记住,集合有contains(),但没有get()UnifiedSetWithHashingStrategy除了实现Set外,还实现了Pool,所以也实现了get()的一种形式。

这是处理不区分大小写字符串的简单方法。

UnifiedSetWithHashingStrategy<String> set = 
  new UnifiedSetWithHashingStrategy<>(HashingStrategies.fromFunction(String::toLowerCase));
set.add("ABC");
Assert.assertTrue(set.contains("ABC"));
Assert.assertTrue(set.contains("abc"));
Assert.assertFalse(set.contains("def"));
Assert.assertEquals("ABC", set.get("aBc"));

这展示了 API,但不适合生产。问题是 HashingStrategy 不断委托给String.toLowerCase(),这会创建一堆垃圾字符串。以下是如何为不区分大小写的字符串创建有效的散列策略。

public static final HashingStrategy<String> CASE_INSENSITIVE =
  new HashingStrategy<String>()
  {
    @Override
    public int computeHashCode(String string)
    {
      int hashCode = 0;
      for (int i = 0; i < string.length(); i++)
      {
        hashCode = 31 * hashCode + Character.toLowerCase(string.charAt(i));
      }
      return hashCode;
    }

    @Override
    public boolean equals(String string1, String string2)
    {
      return string1.equalsIgnoreCase(string2);
    }
  };

注意:我是 GS 系列的开发人员。

【讨论】:

    【解决方案4】:

    Trove4j 具有我所追求的功能,他们称之为散列策略。

    他们的地图具有不同的限制和不同的先决条件的实现,所以这并不隐含地意味着 Java 的“本机”HashMap 的实现是可行的。

    【讨论】:

      【解决方案5】:

      注意:正如所有其他答案中所述,HashMap 没有明确的顺序。他们只承认“平等”。从基于散列的数据结构中得到一个顺序是没有意义的,因为每个对象都会变成一个散列——本质上是一个随机数。

      只要你小心翼翼,你总是可以为一个类写一个散列函数(而且通常是必须的)。这是一件很难正确完成的事情,因为基于散列的数据结构依赖于散列值的随机、均匀分布。在 Effective Java 中,有大量文本专门用于正确实现具有良好行为的哈希方法。

      话虽如此,如果您只是希望您的散列忽略String 的大小写,您可以为此目的围绕String 编写一个包装类,并将其插入您的数据结构中。

      一个简单的实现:

      public class LowerStringWrapper {
          public LowerStringWrapper(String s) {
              this.s = s;
              this.lowerString = s.toLowerString();
          }
      
          // getter methods omitted
      
          // Rely on the hashing of String, as we know it to be good.
          public int hashCode() { return lowerString.hashCode(); }
      
          // We overrode hashCode, so we MUST also override equals. It is required
          // that if a.equals(b), then a.hashCode() == b.hashCode(), so we must
          // restore that invariant.
          public boolean equals(Object obj) {
              if (obj instanceof LowerStringWrapper) {
                  return lowerString.equals(((LowerStringWrapper)obj).lowerString;
              } else {
                  return lowerString.equals(obj);
              }
          }
      
          private String s;
          private String lowerString;
      }
      

      【讨论】:

        【解决方案6】:

        好问题,问 josh bloch。我在 java 7 中将该概念作为 RFE 提交,但它被丢弃了,我相信原因与性能有关。不过我同意,应该这样做。

        【讨论】:

        • 嗯。也许是因为你错过了缓存计算出来的哈希码的机会..
        【解决方案7】:

        我怀疑没有这样做是因为它会阻止 hashCode 缓存?

        我尝试创建一个通用 Map 解决方案,其中所有键都被静默包装。事实证明,包装器必须保存被包装的对象、缓存的 hashCode 和对负责相等检查的回调接口的引用。这显然不如使用包装类有效,在这种包装类中,您只需要缓存原始键和一个对象(请参阅 hazzens 答案)。

        (我还遇到了一个与泛型相关的问题;get 方法接受 Object 作为输入,因此负责散列的回调接口必须执行额外的 instanceof-check。要么这样,要么 map 类必须知道其键的类。)

        【讨论】:

          【解决方案8】:

          这是一个有趣的想法,但它的性能绝对是可怕的。这样做的原因对于idea of a hashtable 来说非常重要:不能依赖排序。哈希表非常快(constant time),因为它们索引表中元素的方式:通过计算该元素的伪唯一整数哈希并访问数组中的该位置。它实际上是计算内存中的位置并直接存储元素。

          这与平衡二叉搜索树 (TreeMap) 形成对比,平衡二叉搜索树 (TreeMap) 必须从根开始并在每次需要查找时向下工作到所需节点。维基百科有一些more in-depth analysis。总而言之,树状图的效率取决于一致的顺序,因此元素的顺序是可预测且合理的。但是,由于“遍历到目的地”方法对性能的影响,BST 只能提供 O(log(n)) 性能。对于大型地图,这可能会严重影响性能。

          可以对哈希表施加一致的顺序,但这样做需要使用类似于LinkedHashMap 的技术并手动维护顺序。或者,可以在内部维护两个独立的数据结构:哈希表和树。该表可用于查找,而树可用于迭代。问题当然是这使用了两倍以上的所需内存。此外,插入的速度仅与树一样快:O(log(n))。并发技巧可以稍微降低这一点,但这不是可靠的性能优化。

          简而言之,您的想法听起来非常好,但如果您真的尝试实现它,您会发现这样做会带来巨大的性能限制。最终的结论是(并且已经持续了几十年):如果您需要性能,请使用哈希表;如果您需要排序并且可以忍受性能下降,请使用平衡二叉搜索树。恐怕真的没有有效地结合这两种结构而不失去其中一种或另一种的一些保证。

          【讨论】:

          • 我认为您的回答与问题没有太大关系。 volley 只想使用用户指定哈希函数的 HashTable,而不是默认的 Object.hashCode()。
          • 不,我认为他想要的远不止这些。他提出的“解决方案”是使用替代散列码强制排序,但这不起作用(散列到有限域中)。要订购哈希表,需要一些辅助结构。
          • 嗯,其实我认为亚当是对的;请注意,我建议的接口包含一种计算哈希的方法和一种检查两个对象是否相等的方法。订单不在里面!比较器是作为类比提到的。 (顺便说一句,喜欢达尔文的标志,丹尼尔!)
          • 你完全错了,看看Hasharator,没有什么比在那里订购更好的了。看看com.google.common.base.Equivalence 和他们的CustomConcurrentHashMap,就是这样。
          • 在告诉我我完全错了之前,可能值得看看问题的编辑历史。我回复的内容与我们现在的内容截然不同。
          【解决方案9】:

          com.google.common.collect.CustomConcurrentHashMap 中有这样的功能,遗憾的是,目前还没有公开的方法来设置Equivalence(他们的Hasharator)。也许他们还没有完成它,也许他们认为该功能还不够有用。询问guava mailing list

          我想知道为什么它还没有发生,就像两年前在talk 中提到的那样。

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2023-03-16
            • 1970-01-01
            • 1970-01-01
            • 2012-06-22
            相关资源
            最近更新 更多