Подтвердить что ты не робот

Мутируемые объекты и hashCode

Введите следующий класс:

public class Member {
private int x;
private long y;
private double d;

public Member(int x, long y, double d) {
    this.x = x;
    this.y = y;
    this.d = d;
}

@Override
public int hashCode() {
    final int prime = 31;
    int result = 1;
    result = prime * result + x;
    result = (int) (prime * result + y);
    result = (int) (prime * result + Double.doubleToLongBits(d));
    return result;
}

@Override
public boolean equals(Object obj) {
    if (this == obj) {
        return true;
    }
    if (obj instanceof Member) {
        Member other = (Member) obj;
        return other.x == x && other.y == y
                && Double.compare(d, other.d) == 0;
    }
    return false;
}

public static void main(String[] args) {
    Set<Member> test = new HashSet<Member>();
    Member b = new Member(1, 2, 3);
    test.add(b);
    System.out.println(b.hashCode());
    b.x = 0;
    System.out.println(b.hashCode());
    Member first = test.iterator().next();
    System.out.println(test.contains(first));
    System.out.println(b.equals(first));
           System.out.println(test.add(first));

}

}

Он производит следующие результаты:
30814 29853 false true true

Поскольку hashCode зависит от состояния объекта, он больше не может быть восстановлен должным образом, поэтому проверка на сдерживание не выполняется. HashSet больше не работает должным образом. Решением было бы сделать Member неизменным, но это единственное решение? Должны ли все классы, добавленные в HashSets, быть неизменными? Есть ли другой способ справиться с ситуацией?

С уважением.

4b9b3361

Ответ 1

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

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

Ответ 2

Да. Поддерживая свой класс mutable, вы можете вычислить методы hashCode и equals, основанные на неизменяемых значениях класса (возможно, сгенерированный id), чтобы придерживаться hashCode, определенный в классе Object:

  • Всякий раз, когда он вызывается на одном и том же объекте более одного раза во время выполнения приложения Java, метод hashCode должен последовательно возвращать одно и то же целое число, если информация, используемая при равных сравнениях с объектом, не изменяется. Это целое число не должно оставаться согласованным с одним исполнением приложения на другое выполнение того же приложения.

  • Если два объекта равны в соответствии с методом equals (Object), то вызов метода hashCode для каждого из двух объектов должен приводить к одному и тому же целочисленному результату.

  • Не требуется, чтобы, если два объекта неравны в соответствии с методом equals (java.lang.Object), то вызов метода hashCode для каждого из двух объектов должен производить различные целочисленные результаты. Тем не менее, программист должен знать, что создание отдельных целочисленных результатов для неравных объектов может улучшить производительность хэш-таблиц.

В зависимости от вашей ситуации это может быть проще или нет.

class Member { 
    private static long id = 0;

    private long id = Member.id++;
    // other members here... 


    public int hashCode() { return this.id; }
    public boolean equals( Object o ) { 
        if( this == o ) { return true; }
        if( o instanceOf Member ) { return this.id == ((Member)o).id; }
        return false;
     }
     ...
 }

Если вам нужен атрибут потокобезопасности, вы можете использовать вместо него: AtomicLong, но опять же, это зависит от того, как вы собираетесь для использования вашего объекта.

Ответ 3

Джон Скит перечислил все альтернативы. Что касается того, почему ключи в Map или Set не должны меняться:

Из договора множества следует, что в любое время не существует двух объектов o1 и o2 таких, что

o1 != o2 && set.contains(o1) && set.contains(o2) && o1.equals(o2)

Почему это необходимо, особенно ясно для Карты. Из договора Map.get():

Более формально, если это отображение содержит отображение из ключа k до значения v, такого, что (key==null ? k==null : key.equals(k)), тогда этот метод возвращает v, в противном случае он возвращает null. (Это может быть не более одного такого отображения.)

Теперь, если вы измените ключ, вставленный в карту, вы можете сделать его равным некоторому уже вставленному ключу. Более того, карта не может знать, что вы это сделали. Итак, что должна делать карта, если вы затем выполняете map.get(key), где key равно нескольким клавишам на карте? Нет никакого интуитивного способа определить, что это значило бы - главным образом потому, что наша интуиция для этих типов данных является математическим идеалом множеств и отображений, которые не имеют отношения к смене ключей, поскольку их ключи являются математическими объектами и, следовательно, неизменяемы.

Ответ 4

Как уже упоминалось, можно принять следующие три решения:

  • Использовать неизменяемые объекты; даже если ваш класс изменен, вы можете использовать неизменяемые идентификаторы в своей реализации hashcode и equals, например, ID-подобное значение.
  • Аналогично описанному выше, реализуйте add/remove, чтобы получить клон вставленного объекта, а не фактическую ссылку. HashSet не предлагает функцию get (например, чтобы впоследствии вы могли изменить объект); таким образом, вы в безопасности, не будет дубликатов.
  • Упражняйте дисциплину, не меняя их после того, как они были использованы, поскольку @Jon Skeet предлагает

Но если по какой-то причине вам действительно нужно изменить объекты после вставки в HashSet, вам нужно найти способ "информировать" вашу коллекцию с новыми изменениями. Для достижения этой функциональности:

  1. Вы можете использовать шаблон проектирования Observer и расширить HashSet для реализации интерфейса Observer. Ваши объекты Member должны быть Observable и update HashSet для любого сеттера или другого метода, который влияет на hashcode и/или equals.

Примечание 1: Расширение 3, используя 4: мы можем принимать изменения, но те, которые не создают уже существующий объект (например, я обновил идентификатор пользователя, назначив новый идентификатор, не устанавливая его на существующий). В противном случае вы должны рассмотреть сценарий, в котором объект преобразуется таким образом, который теперь равен другому объекту, уже существующему в Set. Если вы примете это ограничение, 4-е предложение будет работать нормально, иначе вы должны быть активными и определить политику для таких случаев.

Примечание 2: Вы должны указать как предыдущие, так и текущие состояния измененного объекта в своей реализации update, потому что вы должны сначала удалить старый элемент (например, использовать getClone() перед установкой новых значений), а затем добавить объект с новым состоянием. Следующий фрагмент представляет собой пример реализации, он нуждается в изменениях, основанных на вашей политике добавления дубликата.

@Override
public void update(Observable newItem, Object oldItem) {
    remove(oldItem);
    if (add(newItem))
        newItem.addObserver(this);
}

Я использовал аналогичные методы для проектов, где мне требуется несколько индексов для класса, поэтому я могу искать O (1) для наборов объектов, которые имеют общий идентификатор; представьте это как MultiKeymap из HashSets (это действительно полезно, поскольку вы можете затем пересекать/объединять индексы и работать аналогично поиску, подобному SQL). В таких случаях я комментирую методы (обычно сеттеры), которые должны fireChange обновлять каждый из индексов при значительном изменении, поэтому индексы всегда обновляются с последними состояниями.

Ответ 5

Теоретически (и чаще всего не слишком) ваш класс:

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

Ответ 6

Никогда не меняйте "hashable field" после размещения контейнера на основе hash.

Как если бы вы (участник) зарегистрировали свой номер телефона (Member.x) на желтой странице (контейнер, основанный на хеше), но вы изменили свой номер, то больше никто не сможет найти вас на желтой странице.