Как сломать HashSet в Java?
Года полтора назад работал над одним проектом. Развернут он был на AWS. Сервис на Java работал с БД DynamoDB (NoSQL). После очередной фичи в логах стали появляться ошибки, что приложение не может сохранить данные в БД из-за дублирования ключа. Я был в замешательстве, поскольку в коде для работы с данными использовал HashSet, и был уверен, что дубликаты не могут существовать. Оказалось — еще как могут.
Мы вполне законно можем закинуть объект в HashSet и дальше использовать и модифицировать его в коде. Из-за чего наш объект, находящийся в HashSet, так же меняется. Это связано с тем, что в HashSet хранятся ссылки на объекты, а не сами объекты. Таким образом, два разных с точки зрения места в памяти объекта могут быть совершенно одинаковыми по своему наполнению. И в этом случае переопределение методов equals () и hashCode () не даст нужного эффекта, поскольку они срабатывают при добавлении нового элемента в коллекцию.
Для примера возьмем класс Dog, который имеет два поля — имя и возраст:
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 +
"}";
}
}
А теперь добавим в HashSet два объекта класса Dog — fluffy и jimmy:
Dog fluffy = new Dog("Fluffy", 3);
Dog jimmy = new Dog("Jimmy", 4);
Set 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 переменные бывают двух типов: примитивные и ссылочные. Если с примитивами все понятно — они сразу хранят значение внутри себя, то с ссылочными переменными все несколько сложнее — они лишь хранят ссылку на область памяти, где расположено значение. Соотвественно, меняя поля внутри объекта мы не изменяем ссылку на него.
Поэтому важно запомнить, что любая коллекция фактически хранит в себе ссылку на объект, поэтому при работе с ними следует соблюдать осторожность.
