【问题标题】:Java HashSet contains duplicates if contained element is modified如果包含的元素被修改,Java HashSet 包含重复项
【发布时间】:2012-10-28 23:21:00
【问题描述】:

假设你有一个类并且你创建了一个可以存储这个类的实例的 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 并将内容从一个哈希集复制到另一个哈希集,这样新的哈希集就不会包含重复项,但是我不喜欢这个解决方案。

【问题讨论】:

    标签: java duplicates hashset


    【解决方案1】:

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

    【讨论】:

    • 好的,以上场景无效。我想唯一的选择是将内容复制到一个新的 HashSet 中。
    • @Spi1988 正确的解决方案是遵守Set的约定,添加到集合后不要修改对象。
    • @PB_MLT 通过将内容复制到新的 HashSet 中您将实现什么?
    【解决方案2】:

    补充@EJP 的答案,如果您在HashSet 中改变对象以使它们重复(在equals / hashcode 合同的意义上),实际上会发生什么是哈希表数据结构会破裂。

    • 根据突变的具体细节和哈希表的状态,其中一个或两个实例将变得不可见(例如contains 和其他操作)。要么它在错误的哈希链上,要么因为另一个实例出现在它之前的哈希链上。而且很难预测哪个实例是可见的......以及它是否会保持可见。

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

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


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

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

    从正确性和健壮性的角度来看,第一个选项显然是最好的。


    顺便说一句,要以一般方式“解决”这个问题确实很困难。 Java 中没有普遍的机制来知道……或被通知……某些元素已更改。您可以逐个类地实现这样的机制,但它必须明确编码(而且它不会便宜)。即使你有这样的机制,你会怎么做?显然,现在应该从集合中移除其中一个对象......但是哪一个?

    【讨论】:

    • 感谢您的解释。如果您有一种机制可以检测到集合中的一个对象发生了变化,并且现在等于同一集合中存在的另一个对象,那么您可以删除任何一个重复项(删除哪个都没有关系,因为他们是平等的)。
    • @Spi1988 - “删除哪个并不重要,因为它们是相等的”。一般来说,这是不正确的。 equals() 返回true 的两个对象不必相同。你放弃哪一个可能很重要。您提出的机制是假设性的。
    • 谢谢,我已经为此苦苦挣扎了好几个小时。但老实说,这整个问题的发生只是因为实现太懒了,无法制作正确的 HashSet 而不是仅通过 HashTable 备份它,从而将 hashCode 索引冻结到创建时间。据我所知,他们给我们的这个 HashSet 不是一个 HashSet,而是一个 ImmutableHashSet 和一个正确的 HashSet 实现仍然从 jdk 中丢失,这真的很离谱 - 它缓存了!!!!哇。
    • @PedroBorges 你自己在那里。其他人都可以很好地管理它,而且我见过的所有其他哈希表实现都有相同的行为,包括我自己编写的几个。您实际上并不需要能够修改密钥,如果您这样做,您必须 通知 表:否则它不知道并且它会中断。简单的解决方案是删除-修改-插入。
    【解决方案3】:

    您是对的,我认为没有任何方法可以防止您讨论的案件。所有使用散列和等号的集合都会遇到这个问题。集合没有通知对象自添加到集合后发生了更改。我认为您概述的解决方案很好。

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

    【讨论】:

      【解决方案4】:

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

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

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

      【讨论】:

        【解决方案5】:

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

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

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

        【讨论】:

        【解决方案6】:

        您需要在迭代列表时进行唯一检测。创建一个新的 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();
            }
        }
        

        【讨论】:

        • 他没有清单。他有一套。他在滥用它。没有答案。
        • 他正在使用集合作为列表。所以他需要正确使用集合或使用列表。
        • 他不想要一份清单。他想要一套。他有一套。他在滥用它,然后想知道为什么它的元素不是独一无二的。解决方案不是让它变得更糟,而是从一开始就阻止它发生。
        • 我不想要一个集合(在 Java 中实现)。因为它不适合我的需要。我想要一个对其元素执行的更改敏感的集合,以免任何重复。
        • 缺少一些疯狂的休眠风格运行时挂钩的集合方法,没有办法可以立即对其内容的更改做出反应的集合。按照 EJP 和其他人的建议去做。使用不可变对象并正确使用集合(根据需要添加和删除)。您可以将其包装到您自己的类中,以使调用者更方便。
        猜你喜欢
        • 1970-01-01
        • 2022-07-05
        • 2018-10-02
        • 2023-01-17
        • 1970-01-01
        • 1970-01-01
        • 2013-04-20
        • 2015-01-30
        • 1970-01-01
        相关资源
        最近更新 更多