Java 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 проверить его содержимое, чтобы удалить возможные повторяющиеся записи, созданные в приведенном выше сценарии?

возможным решением может быть создание нового хеш-набора и копирование содержимого из одного хеш-набора в другой, чтобы новый хеш-набор не содержал дубликатов, однако мне не нравится это решение.

6 ответов


ситуация, которую вы описываете, недопустима. Вижу Javadoc: "поведение набора не задается, если значение объекта изменяется таким образом, что влияет на сравнения equals, в то время как объект является элементом набора."


чтобы добавить к ответу @EJP, что произойдет на практике, если вы мутируете объекты в HashSet чтобы сделать их дубликатами (в смысле equals / hashcode контракт) заключается в том, что структура данных хэш-таблицы сломается.

  • в зависимости от точных деталей мутации и состояния хэш-таблицы один или оба экземпляра станут невидимыми для поиска (например,contains и другие операции). Либо он находится в неправильной хэш-цепочке, либо потому, что другой экземпляр появляется перед ним в хэш-цепочке. И трудно предсказать, какой экземпляр будет видно ... и останется ли она видимой.

  • если вы повторите набор, оба экземпляра все равно будут присутствовать ... в нарушение Set контракт.

конечно, это очень разбитая с точки зрения приложения.


вы можете избежать этой проблемы путем:

  • использование неизменяемого тип набора элементов
  • создание копии объектов, как вы положили их в набор и / или вытащить их из набора,
  • написание кода, чтобы он "знал", чтобы не изменять объекты в течение всего времени ...

С точки зрения правильности и надежности, то первый вариант явно лучше.


кстати, было бы очень трудно "исправить" это в общем смысле. В Java нет всепроникающего механизма для знание... или уведомления ... что какой-то элемент изменился. Вы можете реализовать такой механизм на основе класса по классам, но он должен быть закодирован явно (и это не будет дешево). Даже если бы у вас был такой механизм, что бы вы сделали? Ясно, что один из объектов теперь должен быть удален из набора ... но какой?


вы правы, и я не думаю, что есть какой-либо способ защитить от случая, который вы обсуждаете. Все коллекции, использующие хэширование и equals, подвержены этой проблеме. В коллекции нет уведомления об изменении объекта с момента его добавления в коллекцию. Я думаю, что решение, которое вы наметили, хорошее.

Если вы так обеспокоены этой проблемой, возможно, вам нужно переосмыслить свои структуры данных. Например, вы можете использовать неизменяемые объекты. С неизменным объектов у вас не было бы этой проблемы.


HashSet не знает о свойствах своего члена, изменяющихся после добавления объекта. Если это проблема для вас, то вы можете рассмотреть вопрос о создании GraphEdge незыблемыми. Например:

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

в случае GraphEdge является неизменяемым, изменение значения приводит к возвращению нового экземпляра, а не к изменению существующего экземпляра.


объекты.хэш-код предназначен для создания hascode с использованием объектов параметров. Вы используете его как часть вычисления hascode.

попробуйте заменить вашу реализацию хэш-кода следующим:

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

вам нужно будет сделать уникальное обнаружение a во время итерации списка. Создание нового 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();
    }
}