Найти в Дзене
реFUCKторинг

Как сломать HashSet в Java?

Года полтора назад работал над одним проектом. Развернут он был на AWS. Сервис на Java работал с БД DynamoDB (NoSQL). В какой-то момент в логах посыпались ошибки, что приложение не может сохранить данные в БД из-за дублирования ключа. Я был удивлен, поскольку в коде для работы с данными использовал HashSet, и был уверен, что дубликаты не могут существовать. Оказалось - еще как могут. Если при работе, например, со строками HashSet хранит данные как положено, то при работе с объектами ситуация совсем другая. Мы вполне законно можем закинуть объект в HashSet и после этого дальше использовать и модифицировать его в коде. Из-за чего наш объект, находящийся в HashSet, так же меняется. Это связано с тем, что в HashSet хранятся ссылки на объекты, а не сами объекты. Таким образом, два разных с точки зрения места в памяти объекта могут быть совершенно одинаковыми по своему наполнению. Для примера возьмем класс Dog, который имеет два поля - имя и возраст: А теперь добавим в HashSet два объекта к

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

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

Для примера возьмем класс Dog, который имеет два поля - имя и возраст:

Dog.class

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

HashSet example

Если мы выведем в консоль содержимое множества 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 переменные бывают двух типов: примитивные и ссылочные. Если с примитивами все понятно - они сразу хранят значение внутри себя, то с ссылочными переменными все несколько сложнее - они лишь хранят ссылку на область памяти, где расположено значение. Соотвественно, меняя значение объекта мы не изменяем ссылку на него.

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