可变对象和hashCode

有以下课程:

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 test = new HashSet(); Member b = new Member(1, 2, 3); test.add(b); System.out.println(b.hashCode()); bx = 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不再正常工作。 一个解决方案是使成员不可变,但这是唯一的解决方案吗? 是否所有添加到HashSet的类都是不可变的? 有没有其他方法来处理这种情况?

问候。

散列集中的对象应该是不可变的, 或者您需要在哈希集(或hashmap)中使用它们之后不要更改它们。

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

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

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

  • 如果两个对象根据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 ,但同样,它取决于你将如何使用你的对象。

Jon Skeet列出了所有替代品。 至于为什么Map或Set中的键不能改变:

集合的契约意味着在任何时候,没有两个对象o1和o2这样

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

为什么需要这一点对于Map来说尤其明显。 从Map.get()的合同:

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

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

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

  1. 使用不可变对象; 即使您的类是可变的,您也可以在hashcode实现上使用不可变标识并equals检查,例如类似ID的值。
  2. 与上面类似,实现add / remove以获取插入对象的克隆,而不是实际引用。 HashSet不提供get函数(例如,允许您稍后更改对象); 因此,你是安全的,不存在重复。
  3. 正如@ Jon Skeet建议的那样,在使用它们之后不要改变它们的运动纪律

但是,如果由于某种原因你真的需要在插入HashSet后修改对象,你需要找到一种方法“通知”你的集合中的新变化。 要实现此function:

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

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

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

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

我在项目上使用了类似的技术,我需要在一个类上有多个索引,所以我可以用O(1)查找共享一个共同标识的对象集; 想象它作为HashSets的MultiKeymap(这非常有用,因为你可以交叉/联合索引并且类似于类似SQL的搜索工作)。 在这种情况下,我会注释必须fireChange的方法(通常是setter) – 在发生重大更改时更新每个索引,因此索引始终使用最新状态进行更新。

理论上(通常也是实际上也是如此)你的课程:

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

放入基于散列的容器后,切勿更改’hashable field’。

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