"Метод порівняння порушує його загальний контракт!"


187

Чи може хтось пояснити мені простими словами, чому цей код кидає виняток: "Метод порівняння порушує його загальний контракт!", І як це я виправити?

private int compareParents(Foo s1, Foo s2) {
    if (s1.getParent() == s2) return -1;
    if (s2.getParent() == s1) return 1;
    return 0;
}

1
Як називається і клас Винятку? Це незаконний аргументЕксцепція? Якби я мав здогадуватися, я вважав би, що ти повинен робити це s1.getParent().equals(s2)замість s1.getParent() == s2.
Фрайхейт

І виняток, який також кидається.
Меттью Фарвелл

2
Я мало знаю про Java або про API порівняння Java, але цей метод порівняння здається мертвим неправильним. Припустимо, s1це батько s2, а s2не батько s1. Тоді compareParents(s1, s2)є 0, але compareParents(s2, s1)є 1. Це не має сенсу. (Крім того, він не є транзитивним, як, наприклад, згаданий нижче
Aix

4
Здається, що ця помилка створюється лише певною бібліотекою cr.openjdk.java.net/~martin/webrevs/openjdk7/timsort/src/share/…
Пітер Лоурей

в Java ви можете використовувати рівняння (повернути булевий рівень) або порівнятиTo (повернути -1, 0 або +1). Замініть ці функції у вашому класі Foo і після цього ви можете перевірити s1.getParent (). Equals (s2) ...
Mualig

Відповіді:


261

Ваш компаратор не перехідний.

Дозвольте Aбути батьком Bі Bбути батьком C. Так A > Bі B > Cтоді, це має бути так A > C. Однак, якщо ваш компаратор буде посилатися на, Aі Cвін поверне нуль, тобто A == C. Це порушує договір і, отже, кидає виняток.

Більше приємно бібліотеці виявити це і повідомити про це, а не поводитися хаотично.

Один із способів задовольнити вимогу транзитивності compareParents()- це пройти getParent()ланцюг, а не лише дивитися на безпосереднього предка.


3
Введений в Java 7 в java.util.Arrays.sort stackoverflow.com/questions/7849539 / ...
leonbloy

46
Те, що бібліотека виявляє це, є дивним. Хто - то на сонці заслуговує того, щоб викинути гігантське Ласкаво просимо .
Qix - МОНІКА ПОМИЛИЛА

Чи можете ви трохи узагальнити цю відповідь, щоб зробити це питання кориснішим як довідковий пост?
Бернхард Баркер

1
@Qix - наскільки я люблю НД, це було додано в Java 7 під банером Oracle
isapir

1
@isapir Чорт! Хороший улов.
Qix - МОНІКА ПОМИЛИЛА

38

Тільки тому, що це те, що я отримав, коли переглядав цю помилку в Google, моя проблема полягала в тому, що я мав

if (value < other.value)
  return -1;
else if (value >= other.value)
  return 1;
else
  return 0;

value >= other.valueповинен (очевидно) на насправді бути value > other.valueтак , що ви можете повернути 0 з рівними об'єктами.


7
Я повинен додати , що якщо будь-який з ваших valueє NaN (якщо valueє doubleабо float), воно не буде так добре.
Матьє

22

Порушення договору часто означає, що компаратор не надає правильного чи послідовного значення при порівнянні об'єктів. Наприклад, ви можете виконати порівняння рядків і змусити порожні рядки сортувати до кінця за допомогою:

if ( one.length() == 0 ) {
    return 1;                   // empty string sorts last
}
if ( two.length() == 0 ) {
    return -1;                  // empty string sorts last                  
}
return one.compareToIgnoreCase( two );

Але це оминає випадок, коли БОТИ один і два порожні - і в цьому випадку повертається неправильне значення (1 замість 0, щоб показати збіг), а компаратор повідомляє про це як про порушення. Він повинен був бути написаний так:

if ( one.length() == 0 ) {
    if ( two.length() == 0 ) {
        return 0;               // BOth empty - so indicate
    }
    return 1;                   // empty string sorts last
}
if ( two.length() == 0 ) {
    return -1;                  // empty string sorts last                  
}
return one.compareToIgnoreCase( two );

13

Навіть якщо ваше порівнянняTo має теоретичну транзитивність, іноді тонкі помилки псують речі ... наприклад, арифметична помилка з плаваючою комою. Це сталося зі мною. це був мій код:

public int compareTo(tfidfContainer compareTfidf) {
    //descending order
    if (this.tfidf > compareTfidf.tfidf)
        return -1;
    else if (this.tfidf < compareTfidf.tfidf)
        return 1;
    else
        return 0;

}   

Перехідна властивість явно зберігає, але я чомусь отримував IllegalArgumentException. І виявляється, що через дрібні помилки в арифметиці з плаваючою комою, помилки округлення, внаслідок яких перехідна властивість порушується там, де не повинна! Тому я переписав код, щоб врахувати дійсно крихітні відмінності 0, і він спрацював:

public int compareTo(tfidfContainer compareTfidf) {
    //descending order
    if ((this.tfidf - compareTfidf.tfidf) < .000000001)
        return 0;
    if (this.tfidf > compareTfidf.tfidf)
        return -1;
    else if (this.tfidf < compareTfidf.tfidf)
        return 1;
    return 0;
}   

2
Це було корисно! Мій код був логічно нормальним, але сталася помилка через проблему з точністю.
JSong

6

У нашому випадку ми отримували цю помилку, оскільки ми випадково перевернули порядок порівняння s1 і s2. Тож стежте за цим. Це було очевидно складніше, ніж наступне, але це ілюстрація:

s1 == s2   
    return 0;
s2 > s1 
    return 1;
s1 < s2 
    return -1;

3

Java не перевіряє послідовність у суворому розумінні, лише повідомляє вас, якщо у неї виникнуть серйозні проблеми. Крім того, це не дає багато інформації про помилку.

Мене спантеличив те, що відбувається в моєму сортувальнику, і я зробив чітку послідовність перевірки, можливо, це вам допоможе:

/**
 * @param dailyReports
 * @param comparator
 */
public static <T> void checkConsitency(final List<T> dailyReports, final Comparator<T> comparator) {
  final Map<T, List<T>> objectMapSmallerOnes = new HashMap<T, List<T>>();

  iterateDistinctPairs(dailyReports.iterator(), new IPairIteratorCallback<T>() {
    /**
     * @param o1
     * @param o2
     */
    @Override
    public void pair(T o1, T o2) {
      final int diff = comparator.compare(o1, o2);
      if (diff < Compare.EQUAL) {
        checkConsistency(objectMapSmallerOnes, o1, o2);
        getListSafely(objectMapSmallerOnes, o2).add(o1);
      } else if (Compare.EQUAL < diff) {
        checkConsistency(objectMapSmallerOnes, o2, o1);
        getListSafely(objectMapSmallerOnes, o1).add(o2);
      } else {
        throw new IllegalStateException("Equals not expected?");
      }
    }
  });
}

/**
 * @param objectMapSmallerOnes
 * @param o1
 * @param o2
 */
static <T> void checkConsistency(final Map<T, List<T>> objectMapSmallerOnes, T o1, T o2) {
  final List<T> smallerThan = objectMapSmallerOnes.get(o1);

  if (smallerThan != null) {
    for (final T o : smallerThan) {
      if (o == o2) {
        throw new IllegalStateException(o2 + "  cannot be smaller than " + o1 + " if it's supposed to be vice versa.");
      }
      checkConsistency(objectMapSmallerOnes, o, o2);
    }
  }
}

/**
 * @param keyMapValues 
 * @param key 
 * @param <Key> 
 * @param <Value> 
 * @return List<Value>
 */ 
public static <Key, Value> List<Value> getListSafely(Map<Key, List<Value>> keyMapValues, Key key) {
  List<Value> values = keyMapValues.get(key);

  if (values == null) {
    keyMapValues.put(key, values = new LinkedList<Value>());
  }

  return values;
}

/**
 * @author Oku
 *
 * @param <T>
 */
public interface IPairIteratorCallback<T> {
  /**
   * @param o1
   * @param o2
   */
  void pair(T o1, T o2);
}

/**
 * 
 * Iterates through each distinct unordered pair formed by the elements of a given iterator
 *
 * @param it
 * @param callback
 */
public static <T> void iterateDistinctPairs(final Iterator<T> it, IPairIteratorCallback<T> callback) {
  List<T> list = Convert.toMinimumArrayList(new Iterable<T>() {

    @Override
    public Iterator<T> iterator() {
      return it;
    }

  });

  for (int outerIndex = 0; outerIndex < list.size() - 1; outerIndex++) {
    for (int innerIndex = outerIndex + 1; innerIndex < list.size(); innerIndex++) {
      callback.pair(list.get(outerIndex), list.get(innerIndex));
    }
  }
}

Просто виклик методу checkConsitency зі списком параметрів та компаратором.
Мартін

Ваш код не компілюється. Класи Compare, Convert(і потенційно інші) не визначені. Оновіть фрагмент коду на автономному прикладі.
Гілі

Ви повинні зафіксувати помилку друку checkConsi(s)tencyта видалити всі зайві @paramдекларації, щоб зробити код більш читабельним.
Roland Illig

3

У моєму випадку я робив щось подібне:

if (a.someField == null) {
    return 1;
}

if (b.someField == null) {
    return -1;
}

if (a.someField.equals(b.someField)) {
    return a.someOtherField.compareTo(b.someOtherField);
}

return a.someField.compareTo(b.someField);

Те, що я забув перевірити, було, коли а.someField і b.someField є недійсними.


3

Я бачив, що це відбувається в фрагменті коду, де часто повторювана перевірка нульових значень:

if(( A==null ) && ( B==null )
  return +1;//WRONG: two null values should return 0!!!

2

Якщо compareParents(s1, s2) == -1тоді compareParents(s2, s1) == 1очікується. З кодом це не завжди відповідає дійсності.

Зокрема, якщо s1.getParent() == s2 && s2.getParent() == s1. Це лише одна з можливих проблем.


1

Редагування конфігурації VM працювало для мене.

-Djava.util.Arrays.useLegacyMergeSort=true

Перевірте, чи моя спроба допомогти вам із форматуванням нічого не порушила. Я не впевнений щодо -початку запропонованого рішення. Можливо, ви задумали щось на зразок однопозиційного списку пунктів.
Yunnosch

2
Будь ласка, поясніть, як це допомагає в описуваній проблемі. Наразі це практично відповідь лише для коду.
Yunnosch

0

Ви не можете порівнювати подібні дані об'єкта: s1.getParent() == s2- це порівняє посилання на об'єкт. Вам слід перекрити equals functionклас Foo, а потім порівняти їх такs1.getParent().equals(s2)


Ні, насправді я думаю, що ОП намагається сортувати якийсь список, і хоче насправді порівняти посилання.
Едвард Фолк
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.