Как не стоит работать с HashSet в Java?

—

от автора

Данная статья рассчитана на тех, кто только начинает постигать основы языка Java. И людям с опытом может показаться очевидной банальщиной.

Года полтора назад работал над одним проектом. Развернут он был на AWS. Сервис на Java работал с БД DynamoDB (NoSQL). После очередной фичи в логах стали появляться ошибки, что приложение не может сохранить данные в БД из-за дублирования ключа. Я был в замешательстве, поскольку в коде для работы с данными использовал HashSet, и был уверен, что дубликаты не могут существовать. Оказалось — еще как могут.

Мы вполне законно можем закинуть объект в HashSet и дальше использовать и модифицировать его в коде. Из-за чего наш объект, находящийся в HashSet, тоже меняется. Это связано с тем, что в HashSet хранятся ссылки на объекты, а не сами объекты. Таким образом, два разных с точки зрения места в памяти объекта могут быть совершенно одинаковыми по своему наполнению. И в этом случае переопределение методов equals() и hashCode() не даст нужного эффекта, поскольку они срабатывают при добавлении нового элемента в коллекцию.

Для примера возьмем класс Dog, который имеет два поля — имя и возраст со стандартной реализацией equals и hashCode из пакета commons-lang3:

public class Dog {     private String name;     private int age;      public Dog(String name, int age) {         this.name = name;         this.age = age;     }      public String getName() {         return name;     }      public int getAge() {         return age;     }      public void setName(String name) {         this.name = name;     }      public void setAge(int age) {         this.age = age;     }      @Override     public String toString() {         return "Dog{" +                 "name='" + name + '\'' +                 ", age=" + age +                 "}";     }      @Override     public boolean equals(Object o) {         if (this == o) return true;          if (o == null || getClass() != o.getClass()) return false;          Dog dog = (Dog) o;          return new EqualsBuilder().append(age, dog.age).append(name, dog.name).isEquals();     }      @Override     public int hashCode() {         return new HashCodeBuilder(17, 37).append(name).append(age).toHashCode();     } }

А теперь добавим в HashSet два объекта класса Dog — fluffy и jimmy:

Dog fluffy = new Dog("Fluffy", 3); Dog jimmy = new Dog("Jimmy", 4);  Set<Dog> dogs = new HashSet<>(); dogs.add(fluffy); dogs.add(jimmy);

Если мы выведем в консоль содержимое множества dogs, то получим строку вида:

[Dog{name='Fluffy', age=3}, Dog{name='Jimmy', age=4}]

А теперь объекту jimmy изменим значения полей name и age на ‘Fluffy’ и ‘3’. И тут самое интересное: в нашем множестве окажутся два одинаковых по своему наполнению объекта.

[Dog{name='Fluffy', age=3}, Dog{name='Fluffy', age=3}]

Почему так произошло? Здесь стоит вспомнить о типах переменных.

В Java переменные бывают двух типов: примитивные и ссылочные. Если с примитивами все понятно — они сразу хранят значение внутри себя, то с ссылочными переменными все несколько сложнее — они лишь хранят ссылку на область памяти, где расположено значение. Соответственно, меняя поля внутри объекта, мы не изменяем ссылку на него.

Поэтому важно запомнить, что любая коллекция фактически хранит в себе ссылку на объект, поэтому при работе с ними следует соблюдать осторожность.

p.s. при хранении внутри коллекции immutable объектов данной ситуации не случится, но разбор mutable/immutable — это уже другая история.


ссылка на оригинал статьи https://habr.com/ru/articles/767088/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *