2012-01-14 4 views
31

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

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 были сгенерированы Затмении используя оба поля как A, так и B. Проблема заключается в том, что вызов метода на любом из объектов приводит к 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

Зачем вам нужны дети, чтобы иметь ссылки на родитель? Это сделает вашу жизнь очень сложной в отношении сериализации. – I82Much

+2

Я работаю с устаревшей моделью домена. Моя сериализация существенно исключает родительские отношения в ребенке, а затем фиксирует отношения при десериализации. Имеет ли циркулярные ссылки не обычную вещь в моделях домена? Основополагающим отношением к БД является OneToMany, который был обращен инженерами JBoss в объекты JPA с циклическими ссылками. – Tom

ответ

5

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

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

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

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

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

+0

Я не совсем понимаю, как это дублирование информации, как вы говорите. Возможно, вы не имеете строгого отношения к A, в котором вы можете сделать вывод о том, что родитель B является A, потому что на него ссылаются слова A. Вы можете иметь только дескриптор на B и знать что-то об A, на которое оно ссылается, правильно? Эта информация не будет существовать, если у B нет ссылки на A. – Tom

+0

Это зависит от того, что вы делаете; возможно, вам действительно нужно найти родителя B. Но вы, как правило, разрабатываете свой код таким образом, чтобы его можно было избежать. Если вы не можете этого избежать, тогда вы должны быть очень осторожны, чтобы не попасть в противоречивое состояние. – toto2

+0

Я бы сказал, что двусвязный список имеет дублирование данных, и он полезен для эффективности в определенных ситуациях. Однако вы никогда не сможете войти в противоречивое состояние, потому что API таков, что обе стороны собственности всегда изменяются одновременно. – toto2

3

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

@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

Это имеет смысл для меня. В моем случае, однако, мне нужен всеобъемлющий метод equals для моих пользовательских тестов junit для сериализатора. В моих модульных тестах я хотел убедиться, что объект был сериализован и десериализирован правильно, поэтому я тестировал каждое поле. На практике моя модель домена, вероятно, будет в порядке, когда мой метод equals проверяет идентификатор объекта. – Tom

+0

Это помогает в решении моей проблемы с практикой. – Tom

-2

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

+0

Это, вероятно, будет работать, однако я ищу лучшие методы в области доменов и конкретное решение для рекурсивной проблемы. – Tom

0

Возможно, у вас есть два варианта: 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, нуль)
    1. если this.b != null, мы в конечном итоге в третьем блоке, если-то еще, что вызывает b.recursiveEquality(other.b, this)
      1. в B.recursiveEquality, мы попали в первые i f-else block, который просто утверждает, что наш A является тем же, который был нами передан (то есть, что круговая ссылка не нарушена)
      2. мы закончили B.recursiveEquality, проверив aKey (в зависимости от ваших инвариантов, вы можете захотеть утверждать что-то, основанное на том, что произошло на шаге 3).B.recursiveEquality возвращает
    2. мы закончим A.recursiveEquality проверяя bKey, возможно, с аналогичным утверждает
  2. A.equals возвращает результат рекурсивной проверки равенства
+0

Это, вероятно, будет работать, однако я ищу лучшие методы в области доменов и конкретное решение для рекурсивной проблемы. – Tom

Смежные вопросы