Реализация equals и hashCode для объектов с циклическими ссылками в Java - PullRequest
33 голосов
/ 14 января 2012

У меня есть два класса, определенных так, что они оба содержат ссылки на другой объект. Они выглядят примерно так (это упрощено; в моей реальной модели предметной области класс A содержит список B, а каждый B имеет ссылку на родительский объект A):

public class A {

    public B b;
    public String bKey;

    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + ((b == null) ? 0 : b.hashCode());
        result = prime * result + ((bKey == null) ? 0 : bKey.hashCode());
        return result;
    }
    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (!(obj instanceof A))
            return false;
        A other = (A) obj;
        if (b == null) {
            if (other.b != null)
                return false;
        } else if (!b.equals(other.b))
            return false;
        if (bKey == null) {
            if (other.bKey != null)
                return false;
        } else if (!bKey.equals(other.bKey))
            return false;
        return true;
    }
}

public class B {

    public A a;
    public String aKey;

    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + ((a == null) ? 0 : a.hashCode());
        result = prime * result + ((aKey == null) ? 0 : aKey.hashCode());
        return result;
    }
    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (!(obj instanceof B))
            return false;
        B other = (B) obj;
        if (a == null) {
            if (other.a != null)
                return false;
        } else if (!a.equals(other.a))
            return false;
        if (aKey == null) {
            if (other.aKey != null)
                return false;
        } else if (!aKey.equals(other.aKey))
            return false;
        return true;
    }
}

Eclipse сгенерировал hashCode и equals с использованием обоих полей A и B. Проблема в том, что вызов метода equals или hashCode для любого объекта приводит к StackOverflowError, так как они оба вызывают метод equals и hashCode другого объекта. Например, следующая программа завершится с StackOverflowError использованием указанных выше объектов:

    public static void main(String[] args) {

        A a = new A();
        B b = new B();
        a.b = b;
        b.a = a;

        A a1 = new A();
        B b1 = new B();
        a1.b = b1;
        b1.a = a1;

        System.out.println(a.equals(a1));
    }

Если что-то не так с определением модели предметной области с круговыми отношениями, пожалуйста, дайте мне знать. Насколько я могу судить, это довольно распространенный сценарий, верно?

Каков наилучший способ определения hashCode и equals в этом случае? Я хочу сохранить все поля в методе equals, чтобы это было истинное сравнение глубокого равенства объекта, но я не вижу, как я могу справиться с этой проблемой. Спасибо!

Ответы [ 4 ]

5 голосов
/ 14 января 2012

Я согласен с комментарием I82, так как вам следует избегать ссылки B на своих родителей: это дублирование информации, которое обычно приводит только к проблемам, но вам, возможно, потребуется сделать это в вашем случае.

Даже еслиВы оставляете родительскую ссылку в B, что касается хэш-кодов, вы должны полностью игнорировать родительскую ссылку и использовать только внутренние переменные true из B для построения хэш-кода.

A s являются просто контейнерами, и их значение полностью определяется их содержимым, которое является значениями содержащихся B s, как и их хеш-ключи.

ЕслиA - неупорядоченный набор, вы должны быть очень осторожны, чтобы хеш-код, который вы строите из значений B (или B хеш-кодов), не зависел от некоторого порядка.Например, если хеш-код создается путем добавления и умножения хеш-кодов содержащихся B в некоторой последовательности, вы должны сначала упорядочить хеш-коды по возрастанию, прежде чем вычислять результат сумм / умножений.Точно так же A.equals(o) не должен зависеть от порядка B s (если неупорядоченный набор).

Обратите внимание, что если вы используете java.util.Collection в A, то просто исправьте B хэш-код с игнорированием родительской ссылки автоматически даст действительные A хеш-коды, поскольку Collection по умолчанию имеют хорошие хэш-коды (упорядоченные или нет).

4 голосов
/ 14 января 2012

В типичной модели большинство сущностей имеют уникальный идентификатор. Этот идентификатор полезен в различных случаях использования (в частности: получение / просмотр базы данных). IIUC, поле bKey должно быть таким уникальным идентификатором. Таким образом, обычной практикой сравнения таких объектов является сравнение их идентификатора:

@Override
public boolean equals(Object obj) {
    if (obj == null)
        return false;
    if (!getClass().equals(obj.getClass()))
        return false;
    return this.bKey.equals(((B) obj).bKey);
}


@Override
public int hashCode() { return bKey.hashCode(); }

Вы можете спросить: «что произойдет, если два объекта B имеют одинаковый идентификатор, но разное состояние (значения их полей различны)». Ваш код должен убедиться, что такие вещи не происходят. Это будет проблемой независимо от того, как вы реализуете equals() или hashCode(), потому что по сути это означает, что у вас есть две разные версии одной и той же сущности в вашей системе, и вы не сможете определить, какая из них правильная.

0 голосов
/ 14 января 2012

Вы можете иметь два варианта equals - переопределение Object.equals и тот, который лучше подходит для рекурсии.Проверка рекурсивного равенства принимает A или B - в зависимости от того, является ли другой класс этого класса - который вы называете рекурсивным равенством от имени.Если вы звоните по номеру this.equals, вы передаете null.Например:

A {
    ...
    @Override
    public boolean equals(Object obj) {
        // check for this, null, instanceof...
        A other = (A) obj;
        return recursiveEquality(other, null);
    }

    // package-private, optionally-recursive equality
    boolean recursiveEquality(A other, B onBehalfOf) {
        if (onBehalfOf != null) {
            assert b != onBehalfOf;
            // we got here from within a B.equals(..) call, so we just need
            // to check that our B is the same as the one that called us.
        }
        // At this point, we got called from A.equals(Object). So,
        // need to recurse.
        else if (b == null) {
            if (other.b != null)
                return false;
        }
        // B has a similar structure. Call its recursive-aware equality,
        // passing in this for the onBehalfOf
        else if (!b.recursiveEquality(other.b, this))
            return false;

        // check bkey and return
    }
}

Итак, следующие A.equals:

  1. A.equals вызывает `recursiveEquality (otherA, null)
    1. , если this.b != null,мы попадаем в третий блок if-else, который вызывает b.recursiveEquality(other.b, this)
      1. в B.recursiveEquality, мы попадаем в блок first if-else, который просто утверждает, что наш Aтот же, что был передан нам (т. е. круговая ссылка не нарушена)
      2. мы заканчиваем B.recursiveEquality проверкой aKey (в зависимости от ваших инвариантов вы можете утверждать что-то на основена то, что произошло в шаге 3).B.recursiveEquality возвращает
    2. мы заканчиваем A.recursiveEquality проверкой bKey, возможно, с аналогичными утверждениями
  2. A.equals возвращает результатрекурсивная проверка на равенство
0 голосов
/ 14 января 2012

Прежде всего, вы уверены, что хотите переопределить Equals() и GetHashCode()?В большинстве сценариев у вас должно быть все в порядке с ссылочным равенством по умолчанию.

Но, допустим, нет.Чем, какую семантику равенства вы хотите?

Например, скажем, у каждого A есть поле getB типа B, а у каждого B есть поле getA типа A.Пусть a1 и a2 будут двумя A объектами, имеют одинаковые поля и одинаковые getB (такие же, как в «одном и том же адресе памяти») b1.a1 и a2 равны?Предположим, что b1.getA - это то же самое, что и a1 (как и в «том же адресе памяти»), но не то же самое, что и a2.Вы все еще хотите считать a1 и a2 равными?

Если нет, ничего не переопределять и использовать стандартное равенство ссылок.

Если да, то вот решение: пусть A имеет функцию int GetCoreHashCode(), которая не зависит от элемента getB (но зависит от других полей).Пусть B имеет функцию int GetCoreHashCode(), которая не зависит от элемента getA (но зависит от других полей).Теперь пусть int GetHashCode() функция A зависит от this.GetCoreHashCode() и getB.GetCoreHashCode(), а также от B, и все готово.

...