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

时间:2012-10-28 23:21:01

标签: java duplicates hashset

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

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

以下代码演示了这一点:

 public static void main(String[] args)
    {
         HashSet<GraphEdge> 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

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

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

6 个答案:

答案 0 :(得分:16)

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

答案 1 :(得分:3)

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

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

  • 如果您对该集进行迭代,则两个实例仍将存在...违反Set合同。

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


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

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

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


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

答案 2 :(得分:1)

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

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

答案 3 :(得分:1)

在添加对象后,

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

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

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

答案 4 :(得分:-1)

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

尝试使用以下内容替换hashCode的实现:

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

答案 5 :(得分:-1)

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

public class TestIterator {
    public static void main(String[] args) {
        List<String> list = new ArrayList<String>();

        list.add("1");
        list.add("1");
        list.add("2");
        list.add("3");

        for (String s : new UniqueIterator<String>(list)) {
            System.out.println(s);
        }
    }
}

public class UniqueIterator<T> implements Iterable<T> {
    private Set<T> hashSet = new HashSet<T>();

    public UniqueIterator(Iterable<T> iterable) {
        for (T t : iterable) {
            hashSet.add(t);
        }
    }

    public Iterator<T> iterator() {
        return hashSet.iterator();
    }
}