【问题标题】:Un-overiding hashCode取消覆盖 hashCode
【发布时间】:2011-11-02 21:18:14
【问题描述】:

我有以下情况:我有很多BSTs,我想合并同构子树以节省空间。

我将二叉搜索树节点散列成一个“唯一表”——基本上是 BST 节点的散列。

具有相同左右子节点和相同键的节点具有相同的哈希码,并且我已经适当地覆盖了节点类的等于。

一切正常,除了计算哈希值很昂贵 - 它涉及计算子节点的哈希值。

我想缓存一个节点的哈希值。我遇到的问题是这样做的自然方式,从节点到整数的 HashMap 本身会调用节点上的哈希函数。

我通过在节点中声明一个新字段来解决这个问题,我用它来存储哈希码。但是,我觉得这不是正确的解决方案。

我真正想要的是使用使用节点地址的哈希将节点映射到它们的哈希码。我想我可以通过制作 HashMap 并将节点转换为对象来做到这一点,然后它会在对象上调用 hashCode 方法,但这不起作用(插入哈希仍然调用节点哈希和相等函数。

我希望深入了解实现节点以散列代码缓存的最佳方式。我在下面附上了代码,说明了下面发生的事情。

import java.util.Set;
import java.util.HashSet;
import java.util.Map;
import java.util.HashMap;

class Bst {

  int key;
  String name;
  Bst left;
  Bst right;

  public Bst( int k, String name, Bst l, Bst r ) {
    this.key = k;
    this.name = name;
    this.left = l;
    this.right = r;
  }

  public String toString() {
    String l = "";
    String r = "";
    if ( left != null ) {
      l = left.toString();
    }
    if ( right != null ) {
      r = right.toString();
    }
    return key + ":" + name + ":" + l + ":" + r;
  }

 @Override
  public boolean equals( Object o ) {
    System.out.println("calling Bst's equals");
    if ( o == null ) {
      return false;
    }
    if ( !(o instanceof Bst) ) {
      return false;
    }
    Bst n = (Bst) o;

    if ( n == null || n.key != key ) {
      return false;
    } else if ( n.left != null && left == null || n.right != null && right == null ||
                n.left == null & left != null || n.right == null && right != null ) {
      return false;
    } else if ( n.left != null && n.right == null ) {
      return n.left.equals( left );
    } else if ( n.left != null && n.right != null ) {
      return n.left.equals( left ) && n.right.equals( right );
    } else if ( n.left == null && n.right != null ) {
      return n.right.equals( right );
    } else {
      return true;
    }
  }

  @Override
  public int hashCode() {
    // the real hash function is more complex, entails
    // calling hashCode on children if they are not null
    System.out.println("calling Bst's hashCode");
    return key;
  }
}

public class Hashing {

  static void p(String s) { System.out.println(s); }

  public static void main( String [] args ) {
    Set<Bst> aSet = new HashSet<Bst>();
    Bst a = new Bst(1, "a", null, null );
    Bst b = new Bst(2, "b", null, null );
    Bst c = new Bst(3, "c", null, null );
    Bst d = new Bst(1, "d", null, null );

    a.left = b;
    a.right = c;
    d.left = b;
    d.right = c;

    aSet.add( a );
    if ( aSet.contains( d ) ) {
      p("d is a member of aSet");
    } else {
      p("d is a not member of aSet");
    }

    if ( a.equals( d ) ) {
      p("a and d are equal");
    } else {
      p("a and d are not equal");
    }

    // now try casts to objects to avoid calling Bst's HashCode and equals
    Set<Object> bSet = new HashSet<Object>();
    Object foo = new Bst( a.key, a.name, a.left, a.right );
    Object bar = new Bst( a.key, a.name, a.left, a.right );
    bSet.add( foo );
    p("added foo");
   if ( bSet.contains( bar ) ) {
      p("bar is a member of bSet");
    } else {
      p("bar is a not member of bSet");
    }
  }
}

【问题讨论】:

    标签: java overriding equals hashcode superclass


    【解决方案1】:

    将哈希值存储在节点的字段中对我来说完全是正确的解决方案。这也是java.lang.String 用于其自己的哈希码的内容。除此之外,这意味着您不可能最终获得可以收集的对象的缓存条目等。

    如果您真的想要Object 中的实现将返回的hashCode 的值,您可以使用System.identityHashCode。不过,您不应该依赖这个 - 或任何其他哈希码 - 是唯一的。

    还有一点:由于字段是包访问,因此您的树目前是可变的。如果您在第一次调用它时缓存哈希码,您将不会“注意到”它是否会由于字段更改而发生更改。基本上你不应该在使用哈希码后更改节点。

    【讨论】:

    • 感谢您的澄清。我不太关心可能被垃圾收集的条目,因为我可以编写自己的内存管理器并进行适当的清理。我写的代码只是为了说明,我很欣赏你关于不变性的观点。
    • Jon 会给出一个有效警告,在您使用其哈希码后不要更改 a 节点。即使您没有缓存哈希码也是如此,因为在这种情况下,您将更改现有对象的哈希码,从而使其无法访问任何使用哈希码查找对象的容器。引用 Eric Lippert 的 this C# article 的话:当对象包含在依赖于哈希码保持稳定的数据结构中时,[hashCode] 绝不能更改。
    • 是的,那篇文章不是关于 Java 的。但它列出的规则同样适用于 Java 的 hashCode
    【解决方案2】:

    Java 的内置 IdentityHashMap 符合您的描述。

    也就是说,Jon Skeet 的回答听起来更像是正确的方法。

    【讨论】:

    • 刚刚尝试了 IdentityHashMap,效果很好!我同意,尽管将代码保留为节点字段更简单,启动速度更快,但我会坚持下去。
    【解决方案3】:

    将哈希值存储在字段中实际上相当于“缓存”该值,这样就不必过于频繁地重新计算它。

    这不一定是一种不好的做法,但您必须确保在发生更改时正确清除/重新计算它,如果您必须通知复杂图形或树的上下变化,这可能会令人生畏.

    如果您想使用由 JVM 计算的哈希码(大致基于对象的“RAM 地址”,即使它的值是特定于实现的),您可以使用 System.identityHashCode(x),它完全可以以及 Object.hashCode 的作用。

    【讨论】:

    • 谢谢,正如之前的发帖人所提到的,存在关于可变性的问题,但是对于我的应用程序,我只是在构建树后使用树进行查找,因此没有更新。下一张海报指出对象可以四处移动,System.identityHashCode(x) 也会导致缓存损坏。
    • System.identityHashCode() 可能会或可能不会返回对象的实际内存地址(其计算是特定于实现的)。想想当 GC 在内部重定位一个对象时会发生什么......
    • 嗨,Péter,你说得对,但是因为他使用了“地址”这个词,所以这样说更容易让他了解完整的垃圾收集设计。我会澄清答案。
    【解决方案4】:

    我真正想要的是使用使用节点地址的哈希将节点映射到它们的哈希码。

    节点地址是什么意思? Java 中没有这样的概念,并且我知道的对象没有唯一标识符,例如非基于 VM 的语言中的物理地址,例如C++。 Java 中的引用不是内存地址,对象可能随时被 GC 重定位到内存中。

    我认为我可以通过制作 HashMap 并将节点转换为对象来做到这一点,然后对象会调用 hashCode 方法,但这不起作用

    确实,由于hashCode 是虚拟的,并且在您的节点类中被覆盖,因此无论您拥有的引用的静态类型如何,都会始终调用子类实现。

    恐怕任何尝试使用映射来缓存哈希值都会遇到相同的鸡和鸡蛋问题,即 - 正如您所提到的 - 映射首先需要哈希值本身。

    我没有看到比像您那样在节点中缓存哈希值更好的方法了。 您需要确保缓存值在子节点更改时失效。错误 - 正如 Jon 的回答所指出的那样,在对象存储在映射后更改对象的哈希码会破坏映射的内部完整性,所以它一定不会发生。

    【讨论】:

    猜你喜欢
    • 2020-06-06
    • 2018-03-27
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-04-03
    • 1970-01-01
    • 1970-01-01
    • 2012-11-20
    相关资源
    最近更新 更多