如果包含元素被修改,Java HashSet包含重复项

假设您有一个类,并且您创建了一个可以存储此类实例的HashSet。 如果您尝试添加相同的实例,则集合中只保留一个实例,这很好。

但是,如果在HashSet中有两个不同的实例,并且您获取一个并使其成为另一个的精确副本(通过复制字段),则HashSet将包含两个重复的实例。

以下代码演示了这一点:

public static void main(String[] args) { HashSet set = new HashSet(); GraphEdge edge1 = new GraphEdge(1, "a"); GraphEdge edge2 = new GraphEdge(2, "b"); GraphEdge edge3 = new GraphEdge(3, "c"); set.add(edge1); set.add(edge2); set.add(edge3); edge2.setId(1); edge2.setName("a"); for(GraphEdge edge: set) { System.out.println(edge.toString()); } if(edge2.equals(edge1)) { System.out.println("Equals"); } else { System.out.println("Not Equals"); } } public class GraphEdge { private int id; private String name; //Constructor ... //Getters & Setters... public int hashCode() { int hash = 7; hash = 47 * hash + this.id; hash = 47 * hash + Objects.hashCode(this.name); return hash; } public boolean equals(Object o) { if(o == this) { return true; } if(o instanceof GraphEdge) { GraphEdge anotherGraphEdge = (GraphEdge) o; if(anotherGraphEdge.getId() == this.id && anotherGraphEdge.getName().equals(this.name)) { return true; } } return false; } } 

上面代码的输出:

 1 a 1 a 3 c Equals 

有没有办法强制HashSetvalidation其内容,以便删除上述场景中创建的可能重复条目?

一种可能的解决方案是创建一个新的HashSet并将内容从一个hashset复制到另一个hashset,这样新的hashset就不会包含重复项,但是我不喜欢这个解决方案。

您描述的情况无效。 请参阅Javadoc :“如果对象的值以影响等于比较的方式更改,而对象是集合中的元素,则不指定集合的​​行为。”

要添加到@ EJP的答案,如果你改变HashSet对象以使它们重复(在equals / hashcode契约的意义上),实际上会发生什么,哈希表数据结构将会中断。

  • 根据突变的确切细节和哈希表的状态,一个或两个实例将变为查找不可见(例如contains和其他操作)。 它是在错误的哈希链上,还是因为另一个实例在哈希链上出现在它之前。 并且很难预测哪个实例可见……以及它是否仍然可见。

  • 如果您迭代该集合,则两个实例仍然存在…违反了Set契约。

当然,从应用程序的角度来看,这是非常糟糕的。


您可以通过以下任一方式避免此问题

  • 为set元素使用不可变类型,
  • 在将对象放入集合中时制作对象的副本和/或将对象拉出集合,
  • 编写代码,使其“知道”不要在持续时间内更改对象…

从正确性和稳健性的角度来看,第一种选择显然是最好的。


顺便说一下,以一般方式“修复”这将是非常困难的。 在Java中没有普遍的机制来了解……或被通知……某些元素已经改变。 您可以逐类实现这样的机制,但必须明确编码(并且它不便宜)。 即使你确实有这样的机制,你会做什么? 显然,现在应该从集合中删除其中一个对象……但是哪一个?

你是对的,我认为没有办法防止你讨论的案件。 所有使用散列和等号的集合都会遇到此问题。 该集合没有通知该对象自添加到集合后已更改。 我认为你提出的解决方案很好。

如果您对这个问题如此关注,也许您需要重新考虑您的数据结构。 例如,您可以使用不可变对象。 使用不可变对象,您不会遇到此问题。

添加对象后, HashSet不知道其成员的属性发生了变化。 如果这对您来说是个问题,那么您可能需要考虑使GraphEdge不可变。 例如:

 GraphEdge edge4 = edge2.changeName("new_name"); 

GraphEdge不可变的情况下,更改值会导致返回新实例而不是更改现有实例。

Objects.hashCode用于使用参数对象生成hascode。 您正在使用它作为hascode计算的一部分。

尝试使用以下代码替换hashCode的实现:

 public int hashCode() { return Objects.hashCode(this.id, this.name); } 

您需要在迭代列表时进行唯一检测。 制作一个新的HashSet似乎不是正确的方法,但为什么不尝试这个……也许不使用HashSet开始……

 public class TestIterator { public static void main(String[] args) { List list = new ArrayList(); list.add("1"); list.add("1"); list.add("2"); list.add("3"); for (String s : new UniqueIterator(list)) { System.out.println(s); } } } public class UniqueIterator implements Iterable { private Set hashSet = new HashSet(); public UniqueIterator(Iterable iterable) { for (T t : iterable) { hashSet.add(t); } } public Iterator iterator() { return hashSet.iterator(); } }