有以下课程:
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不再正常工作。解决方案是使成员不可变,但这是唯一的解决方案吗?是否所有添加到HashSet的类都是不可变的?有没有其他方法来处理这种情况?
的问候。
答案 0 :(得分:32)
散列集中的对象 / p>
在实践中,我很少发现这是一个问题 - 我很少发现自己需要使用复杂的对象,因为键是设置元素,而当我这样做时,通常不是一个问题,只是不要改变它们。当然,如果你此时已经公开了对其他代码的引用,那么它会变得更难。
答案 1 :(得分:9)
是。在维护类可变的同时,可以根据类的不可变值(可能是生成的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,但同样,它取决于您将如何使用您的对象。
答案 2 :(得分:3)
Jon Skeet列出了所有替代品。至于为什么Map或Set中的键不能改变:
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}}等于地图中的多个键,地图应该怎么做?没有直观的方法来定义这意味着什么 - 主要是因为我们对这些数据类型的直觉是集合和映射的数学理想,它们不必处理更改键,因为它们的键是数学对象,因此是不可变的。 / p>
答案 3 :(得分:3)
如前所述,人们可以接受以下三种解决方案:
hashcode
实现和equals
检查中使用不可变身份,例如类似ID的值。add
/ remove
以获取插入对象的克隆,而不是实际引用。 HashSet
不提供get
函数(例如,允许您稍后更改对象);因此,你很安全,不会存在重复。但是,如果由于某种原因你真的需要在插入HashSet
后修改对象,你需要找到一种方法来通知&#34;您的收藏与新的变化。要实现此功能:
HashSet
以实现Observer
接口。对于影响Member
和/或Observable
的任何设置者或其他方法,您的update
个对象必须为HashSet
和hashcode
equals
。 注1:扩展3,使用4:我们可以接受更改,但是那些不创建已存在对象的更改(例如,我通过分配新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) - 当发生重大变化时更新每个索引,因此索引总是用最新的状态更新。
答案 4 :(得分:0)
理论上(通常也是实际上也是如此)你的课程:
hashCode
。Set
来存储它们是不必要的,您也可以使用List
。答案 5 :(得分:-1)
在放入基于散列的容器后,切勿更改'hashable field'。
好像您(会员)在黄页(基于散列的容器)中注册了您的电话号码(Member.x),但您更改了号码,然后再也没有人能够在黄页中找到您。