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

Реализация equals и hashCode для объектов с круговыми ссылками в Java

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

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

4b9b3361

Ответ 1

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

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

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

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

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

Ответ 2

В типичной модели большинство объектов имеют уникальный идентификатор. Этот идентификатор полезен в различных случаях использования (в частности: восстановление базы данных/поиск). 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(), потому что по существу это означает, что у вас есть две разные версии одного и того же объекта в вашей системе, и вы не сможете определить, что является правильным.

Ответ 4

У вас может быть два варианта 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:

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

Ответ 5

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