Данная статья рассчитана на тех, кто только начинает постигать основы языка 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 - это уже другая история.
