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


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.