【问题标题】:Mutable objects and hashCode可变对象和 hashCode
【发布时间】:2011-06-10 17:17:34
【问题描述】:

有以下类:

public class Member {
private int x;
private long y;
private double d;

public Member(int x, long y, double d) {
    this.x = x;
    this.y = y;
    this.d = d;
}

@Override
public int hashCode() {
    final int prime = 31;
    int result = 1;
    result = prime * result + x;
    result = (int) (prime * result + y);
    result = (int) (prime * result + Double.doubleToLongBits(d));
    return result;
}

@Override
public boolean equals(Object obj) {
    if (this == obj) {
        return true;
    }
    if (obj instanceof Member) {
        Member other = (Member) obj;
        return other.x == x && other.y == y
                && Double.compare(d, other.d) == 0;
    }
    return false;
}

public static void main(String[] args) {
    Set<Member> test = new HashSet<Member>();
    Member b = new Member(1, 2, 3);
    test.add(b);
    System.out.println(b.hashCode());
    b.x = 0;
    System.out.println(b.hashCode());
    Member first = test.iterator().next();
    System.out.println(test.contains(first));
    System.out.println(b.equals(first));
           System.out.println(test.add(first));

}

}

它产生以下结果:
30814 29853 false true true

因为 hashCode 依赖于对象的状态,它不能再被正确地检索,所以包含检查失败。 HashSet 不再正常工作。一个解决方案是让 Member 不可变,但这是唯一的解决方案吗?添加到 HashSets 的所有类都应该是不可变的吗?有没有其他办法处理这种情况?

问候。

【问题讨论】:

  • 为什么编译器不强制执行不允许可变对象作为哈希表中的键或哈希集中的对象的规则?这对我来说似乎很糟糕。

标签: java immutability hashcode hashset


【解决方案1】:

散列集中的对象应该要么是不可变的,或者您需要遵守纪律,不要在它们被用于散列集(或散列映射)后对其进行更改。

在实践中,我很少发现这是一个问题——我很少发现自己需要使用复杂的对象作为键或设置元素,而且当我这样做时,不改变它们通常不是问题。当然,如果此时您已经公开了对其他代码的引用,这可能会变得更加困难。

【讨论】:

  • 对于值对象。参考对象可以随心所欲地变化。
  • @Tom Hawtin:改变引用对象不是也不安全吗?特别是,我谈到会改变对象的哈希码或使其成为不同等价类的成员的突变。
  • @Brian:我怀疑 Tom 正在谈论不覆盖 hashCode 或 equals 的类型。
  • @Muno:我应该说“驻留在哈希集中时不应发生变异”,但假设一个对象有一组具有关联 XY 坐标的哈希对象。如果每个哈希表应该只为任何给定位置保存一个对象,则有时将位置作为键可能很有用,在这种情况下,哈希集可以确定它是否包含给定 XY 坐标对处的任何对象。但是,如果散列表中对象的 XY 坐标发生移动,则可能无法再从散列表中检索该对象。
  • @Muno:但是,有时使用哈希表将信息与对象关联可能会有所帮助。例如,一个对象可能希望有一个哈希表,列出以某种方式连接到它的所有其他对象,并且不希望仅仅因为某些 XY 坐标发生变化而使这种连接的性质发生变化。在这种情况下,表格将用于封装对象的标识,而不是它们状态的一个方面(例如它们的位置)。
【解决方案2】:

是的。在保持类可变的同时,您可以根据类的不可变值(可能是生成的 id )计算 hashCode 和 equals 方法,以遵守 Object 类中定义的 hashCode 契约:

  • 只要在 Java 应用程序执行期间对同一个对象多次调用它,hashCode 方法必须始终返回相同的整数,前提是没有修改对象上相等比较中使用的信息。该整数不需要在应用程序的一次执行与同一应用程序的另一次执行之间保持一致。

  • 如果两个对象根据equals(Object)方法相等,那么对两个对象中的每一个调用hashCode方法必须产生相同的整数结果。

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

根据您的情况,这可能会更容易。

class Member { 
    private static long id = 0;

    private long id = Member.id++;
    // other members here... 

     
    public int hashCode() { return this.id; }
    public boolean equals( Object o ) { 
        if( this == o ) { return true; }
        if( o instanceOf Member ) { return this.id == ((Member)o).id; }
        return false;
     }
     ...
 }

如果您需要线程安全属性,您可以考虑使用:AtomicLong,但同样,这取决于您将如何使用您的对象。

【讨论】:

  • 这种方法的问题在于,如果我在 HashSet 中插入两个不同的实例(例如 new Member(1,2, 3)),它们将被插入两次。此外,equals 方法也不会按预期工作。但是感谢您指出 hashCode 合约:“在 Java 应用程序执行期间,只要在同一个对象上多次调用它,hashCode 方法必须始终返回相同的整数,前提是对象上的 equals 比较中没有使用任何信息是修改的。”我读得不够仔细。
  • 这只是一个示例,您可以这样创建它:public Member( int a, int b, int ) { this.id = a * 31 + b * 31 + 31 *; } 如果您创建另一个 new Member(1,2,3),它将计算相同的哈希值,但是如果您修改其中之一,您必须同步。您可以使用注册表/工厂方法并创建对象,例如:Member a = Member.newInstance(1,2,3); Member b = Member.newInstance(1,2,3);,并且只创建一次实例。
  • 这种方法的问题在于它基本上与使用 == 运算符进行相等没有什么不同。每个实例都有一个唯一的长 id 并且比较它与比较对象实例引用,基本上是一样的。它不测试内容的相等性。如果他们的情况需要内容平等,这种解决方案是不够的
【解决方案3】:

从理论上(而且通常在实践中也是如此)您的班级:

  1. 具有可以从其字段子集推断出的自然不可变身份,在这种情况下,您可以使用这些字段生成 hashCode 来源。
  2. 没有自然身份,在这种情况下使用Set来存储它们是不必要的,你也可以使用List

【讨论】:

    【解决方案4】:

    Jon Skeet 列出了所有备选方案。至于为什么 Map 或 Set 中的键不能改变:

    集合的契约意味着在任何时候都不存在两个对象 o1 和 o2 使得

    o1 != o2 && set.contains(o1) && set.contains(o2) && o1.equals(o2)
    

    为什么需要这样做对于地图来说尤其清楚。来自 Map.get() 的合约:

    更正式地说,如果此映射包含来自键的映射 k 到一个值v 使得(key==null ? k==null : key.equals(k)),则此方法返回v,否则返回null。 (最多可以有一个这样的映射。)

    现在,如果您修改插入到地图中的键,您可能会使其与已插入的其他键相等。而且,地图无法知道你已经这样做了。那么如果你接着做map.get(key),那么map 应该怎么做,其中key 等于map 中的几个key?没有直观的方法来定义这意味着什么 - 主要是因为我们对这些数据类型的直觉是集合和映射的数学理想,它们不必处理更改键,因为它们的键是数学对象,因此是不可变的。

    【讨论】:

      【解决方案5】:

      放入基于散列的容器后,切勿更改“可散列字段”。

      好像你(会员)在黄页(基于哈希的容器)中注册了你的电话号码(Member.x),但是你改变了你的号码,那么就再也没有人能在黄页找到你了。

      【讨论】:

        【解决方案6】:

        如前所述,可以接受以下三种解决方案:

        1. 使用不可变对象;即使您的类是可变的,您也可以在 hashcode 实现和 equals 检查中使用不可变身份,例如类似 ID 的值。
        2. 与上面类似,实现add/remove 获取插入对象的克隆,而不是实际引用。 HashSet 不提供get 功能(例如,允许您稍后更改对象);因此,您是安全的,不会存在重复项。
        3. 按照@Jon Skeet 的建议,在使用后不要更改它们,遵守纪律

        但是,如果由于某种原因您确实需要在插入HashSet 后修改对象,您需要找到一种方法来“通知”您的 Collection 新更改。要实现此功能:

        1. 您可以使用观察者设计模式,并扩展HashSet 来实现Observer 接口。您的Member 对象必须是Observableupdate 以及影响hashcode 和/或equals 的任何setter 或其他方法上的HashSet

        注 1:扩展 3,使用 4:我们可以接受更改,但那些不创建现有对象的更改(例如,我通过分配新 ID 更新了用户 ID,而不是将其设置为现有对象)。否则,您必须考虑以这样一种方式转换对象的情况,即现在等于Set 中已经存在的另一个对象。如果您接受此限制,则第 4 条建议可以正常工作,否则您必须积极主动并为此类情况制定政策。

        注意 2:您必须在 update 实现中提供更改对象的先前和当前状态,因为您必须首先删除旧元素(例如,在设置新值之前使用 getClone()),然后添加具有新状态的对象。以下 sn-p 只是一个示例实现,它需要根据您添加副本的策略进行更改。

        @Override
        public void update(Observable newItem, Object oldItem) {
            remove(oldItem);
            if (add(newItem))
                newItem.addObserver(this);
        }
        

        我在项目中使用了类似的技术,我需要一个类的多个索引,因此我可以使用 O(1) 查找具有共同身份的对象集;将其想象为 HashSets 的 MultiKeymap(这非常有用,因为您可以相交/联合索引并类似于类似 SQL 的搜索)。在这种情况下,我会注释方法(通常是设置器),当发生重大变化时必须触发Change-更新每个索引,因此索引始终使用最新状态进行更新。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2015-01-14
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2011-07-11
          • 2020-10-29
          相关资源
          最近更新 更多