Изменяемые объекты и hashCode - PullRequest
18 голосов
/ 18 января 2011

Имеют следующий класс:

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 больше не работает должным образом.Решение состоит в том, чтобы сделать члена неизменным, но разве это единственное решение?Должны ли все классы, добавленные в HashSets, быть неизменяемыми?Есть ли другой способ справиться с ситуацией?

С уважением.

Ответы [ 6 ]

32 голосов
/ 18 января 2011

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

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

9 голосов
/ 18 января 2011

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

  • Всякий раз, когда он вызывается для одного и того же объекта более одного раза во время выполнения приложения 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 голосов
/ 23 декабря 2015

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

  1. использовать неизменяемые предметы; даже когда ваш класс изменчив, вы можете использовать неизменные идентификаторы в вашей hashcode реализации и equals проверке, например, в виде идентичного значения.
  2. Аналогично приведенному выше, реализуйте add / remove, чтобы получить клон вставленного объекта, а не фактическую ссылку. HashSet не предлагает функцию get (например, чтобы позже вы могли изменить объект); таким образом, вы в безопасности, дубликатов не будет.
  3. Соблюдайте дисциплину, не меняя их после того, как они были использованы, как @ Джон Скит предлагает

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

3 голосов
/ 18 января 2011

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

Контракт набора подразумевает, что в любое время не существует двух объектов 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 равно нескольким ключам на карте? Не существует интуитивно понятного способа определить, что это будет означать, главным образом потому, что наша интуиция для этих типов данных - это математический идеал множеств и отображений, которым не приходится иметь дело с изменением ключей, поскольку их ключи являются математическими объектами и, следовательно, неизменяемыми.

0 голосов
/ 18 января 2011

Никогда не меняйте «хешируемое поле» после помещения в контейнер на основе хеша.

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

0 голосов
/ 18 января 2011

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

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