Ошибка Java: метод сравнения нарушает его общий контракт

Я видел много вопросов по этому поводу и пытался решить проблему, но после часа поиска в Google и большого количества проб и ошибок я все еще не могу это исправить. Я надеюсь, что некоторые из вас поймут проблему.

Вот что я получаю:

java.lang.IllegalArgumentException: Comparison method violates its general contract!
    at java.util.ComparableTimSort.mergeHi(ComparableTimSort.java:835)
    at java.util.ComparableTimSort.mergeAt(ComparableTimSort.java:453)
    at java.util.ComparableTimSort.mergeForceCollapse(ComparableTimSort.java:392)
    at java.util.ComparableTimSort.sort(ComparableTimSort.java:191)
    at java.util.ComparableTimSort.sort(ComparableTimSort.java:146)
    at java.util.Arrays.sort(Arrays.java:472)
    at java.util.Collections.sort(Collections.java:155)
    ...

И это мой компаратор:

@Override
public int compareTo(Object o) {
    if(this == o){
        return 0;
    }

    CollectionItem item = (CollectionItem) o;

    Card card1 = CardCache.getInstance().getCard(cardId);
    Card card2 = CardCache.getInstance().getCard(item.getCardId());

    if (card1.getSet() < card2.getSet()) {
        return -1;
    } else {
        if (card1.getSet() == card2.getSet()) {
            if (card1.getRarity() < card2.getRarity()) {
                return 1;
            } else {
                if (card1.getId() == card2.getId()) {
                    if (cardType > item.getCardType()) {
                        return 1;
                    } else {
                        if (cardType == item.getCardType()) {
                            return 0;
                        }
                        return -1;
                    }
                }
                return -1;
            }
        }
        return 1;
    }
}

Любая идея?

14 ответов

Решение

Сообщение об исключении на самом деле довольно наглядно. Контракт, который он упоминает, является переходным: если A > B а также B > C тогда для любого A, B а также C: A > C, Я проверил это бумагой и карандашом, и в вашем коде, похоже, есть несколько дыр:

if (card1.getRarity() < card2.getRarity()) {
  return 1;

ты не вернешься -1 если card1.getRarity() > card2.getRarity(),


if (card1.getId() == card2.getId()) {
  //...
}
return -1;

Вы возвращаетесь -1 если идентификаторы не равны. Вы должны вернуться -1 или же 1 в зависимости от того, какой идентификатор был больше.


Взгляните на это. Помимо того, что он гораздо более читабелен, я думаю, что он действительно должен работать:

if (card1.getSet() > card2.getSet()) {
    return 1;
}
if (card1.getSet() < card2.getSet()) {
    return -1;
};
if (card1.getRarity() < card2.getRarity()) {
    return 1;
}
if (card1.getRarity() > card2.getRarity()) {
    return -1;
}
if (card1.getId() > card2.getId()) {
    return 1;
}
if (card1.getId() < card2.getId()) {
    return -1;
}
return cardType - item.getCardType();  //watch out for overflow!

Вы можете использовать следующий класс для определения ошибок транзитивности в ваших компараторах:

/**
 * @author Gili Tzabari
 */
public final class Comparators
{
    /**
     * Verify that a comparator is transitive.
     *
     * @param <T>        the type being compared
     * @param comparator the comparator to test
     * @param elements   the elements to test against
     * @throws AssertionError if the comparator is not transitive
     */
    public static <T> void verifyTransitivity(Comparator<T> comparator, Collection<T> elements)
    {
        for (T first: elements)
        {
            for (T second: elements)
            {
                int result1 = comparator.compare(first, second);
                int result2 = comparator.compare(second, first);
                if (result1 != -result2)
                {
                    // Uncomment the following line to step through the failed case
                    //comparator.compare(first, second);
                    throw new AssertionError("compare(" + first + ", " + second + ") == " + result1 +
                        " but swapping the parameters returns " + result2);
                }
            }
        }
        for (T first: elements)
        {
            for (T second: elements)
            {
                int firstGreaterThanSecond = comparator.compare(first, second);
                if (firstGreaterThanSecond <= 0)
                    continue;
                for (T third: elements)
                {
                    int secondGreaterThanThird = comparator.compare(second, third);
                    if (secondGreaterThanThird <= 0)
                        continue;
                    int firstGreaterThanThird = comparator.compare(first, third);
                    if (firstGreaterThanThird <= 0)
                    {
                        // Uncomment the following line to step through the failed case
                        //comparator.compare(first, third);
                        throw new AssertionError("compare(" + first + ", " + second + ") > 0, " +
                            "compare(" + second + ", " + third + ") > 0, but compare(" + first + ", " + third + ") == " +
                            firstGreaterThanThird);
                    }
                }
            }
        }
    }

    /**
     * Prevent construction.
     */
    private Comparators()
    {
    }
}

Просто вызвать Comparators.verifyTransitivity(myComparator, myCollection) перед кодом, который терпит неудачу.

Это также имеет отношение к версии JDK. Если это хорошо работает в JDK6, возможно, у него будет проблема в JDK 7, описанная вами, потому что метод реализации в jdk 7 был изменен.

Посмотри на это:

Описание: алгоритм сортировки, используемый java.util.Arrays.sort и (косвенно) java.util.Collections.sort был заменен. Новая реализация сортировки может выдать IllegalArgumentException если он обнаруживает Comparable что нарушает Comparable контракт. Предыдущая реализация молча игнорировала такую ​​ситуацию. Если предыдущее поведение желательно, вы можете использовать новое системное свойство, java.util.Arrays.useLegacyMergeSort, чтобы восстановить предыдущее поведение слияния.

Я не знаю точную причину. Однако, если вы добавите код, прежде чем использовать сортировку. Все будет хорошо.

System.setProperty("java.util.Arrays.useLegacyMergeSort", "true");

Рассмотрим следующий случай:

Первый, o1.compareTo(o2) называется. card1.getSet() == card2.getSet() случается, чтобы быть правдой и так card1.getRarity() < card2.getRarity()так вы возвращаете 1.

Затем, o2.compareTo(o1) называется. Снова, card1.getSet() == card2.getSet() правда. Затем вы переходите к следующему else, затем card1.getId() == card2.getId() случается, чтобы быть правдой, и так cardType > item.getCardType(), Вы возвращаете 1 снова.

От этого, o1 > o2, а также o2 > o1, Вы нарушили контракт.

У меня был такой же симптом. Для меня оказалось, что другой поток изменял сравниваемые объекты, пока сортировка происходила в Stream. Чтобы решить эту проблему, я сопоставил объекты с неизменяемыми временными объектами, собрал поток во временную коллекцию и произвел сортировку.

Если вы попытаетесь запустить этот код, вы встретите такое исключение:

          public static void main(String[] args) {
        Random random = new Random();
        List<Integer> list = new ArrayList<>();
        for (int i = 0; i < 50000; i++) {
            list.add(random.nextInt());
        }
        list.sort((x, y) -> {
            int c = random.nextInt(3);
            if (c == 0) {
                return 0;
            }
            if (c == 1) {
                return 1;
            }
            return -1;
        });
    }
      Exception in thread "main" java.lang.IllegalArgumentException: Comparison method violates its general contract!
    at java.util.TimSort.mergeLo(TimSort.java:777)
    at java.util.TimSort.mergeAt(TimSort.java:514)
    at java.util.TimSort.mergeCollapse(TimSort.java:441)
    at java.util.TimSort.sort(TimSort.java:245)
    at java.util.Arrays.sort(Arrays.java:1512)
    at java.util.ArrayList.sort(ArrayList.java:1462)
    at Test.main(Test.java:14)

Причина в том, что при реализации компаратора он может соответствовать случаю A> B и B> C и C> A, и метод сортировки будет запущен, чтобы быть нарушенным. Java предотвращает этот случай, бросая исключение в этом случае:

      class TimSort<T> {
.
.
.
else if (len1 == 0) {
            throw new IllegalArgumentException(
                "Comparison method violates its general contract!");
.
.
.

В заключение, чтобы справиться с этим вопросом. Вы должны убедиться, что компаратор не будет соответствовать случаю A> B и B> C и C> A.

        if (card1.getRarity() < card2.getRarity()) {
            return 1;

Однако если card2.getRarity() меньше чем card1.getRarity() вы не можете вернуть -1.

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

public int compareTo(Object o) {    
    if(this == o){
        return 0;
    }

    CollectionItem item = (CollectionItem) o;

    Card card1 = CardCache.getInstance().getCard(cardId);
    Card card2 = CardCache.getInstance().getCard(item.getCardId());
    int comp=card1.getSet() - card2.getSet();
    if (comp!=0){
        return comp;
    }
    comp=card1.getRarity() - card2.getRarity();
    if (comp!=0){
        return comp;
    }
    comp=card1.getSet() - card2.getSet();
    if (comp!=0){
        return comp;
    }   
    comp=card1.getId() - card2.getId();
    if (comp!=0){
        return comp;
    }   
    comp=card1.getCardType() - card2.getCardType();

    return comp;

    }
}

Это также может быть ошибка OpenJDK... (не в этом случае, но это та же ошибка)

Если кто-то вроде меня наткнется на этот ответ относительно

java.lang.IllegalArgumentException: Comparison method violates its general contract!

тогда это также может быть ошибка в Java-версии. У меня уже несколько лет работает compareTo в некоторых приложениях. Но внезапно он перестал работать и выдает ошибку после того, как все сравнения были выполнены (я сравниваю 6 атрибутов, прежде чем вернуть "0").

Я только что нашел этот отчет об ошибке OpenJDK:

Происхождение этого исключения является неправильным Comparatorреализация. Проверяя документы , мы должны реализовать compare(o1, o2)метод как отношение эквивалентности , следуя правилам:

  • a.equals(b) — сравнение (a, b) — 0
  • a.compare(b) > 0 b.compare(a) < 0
  • ifa.compare(b) > 0 и b.compare(c) > 0 thena.compare(c) > 0 true

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

Я получил ту же ошибку с классом, как следующий StockPickBean, Вызывается из этого кода:

List<StockPickBean> beansListcatMap.getValue();
beansList.sort(StockPickBean.Comparators.VALUE);

public class StockPickBean implements Comparable<StockPickBean> {
    private double value;
    public double getValue() { return value; }
    public void setValue(double value) { this.value = value; }

    @Override
    public int compareTo(StockPickBean view) {
        return Comparators.VALUE.compare(this,view); //return 
        Comparators.SYMBOL.compare(this,view);
    }

    public static class Comparators {
        public static Comparator<StockPickBean> VALUE = (val1, val2) -> 
(int) 
         (val1.value - val2.value);
    }
}

После получения той же ошибки:

java.lang.IllegalArgumentException: метод сравнения нарушает его общий контракт!

Я изменил эту строку:

public static Comparator<StockPickBean> VALUE = (val1, val2) -> (int) 
         (val1.value - val2.value);

чтобы:

public static Comparator<StockPickBean> VALUE = (StockPickBean spb1, 
StockPickBean spb2) -> Double.compare(spb2.value,spb1.value);

Это исправляет ошибку.

Я столкнулся с аналогичной проблемой, когда пытался отсортировать n x 2 2D array названный contestsкоторый представляет собой двумерный массив простых целых чисел. Это работало в большинстве случаев, но вызывало ошибку времени выполнения для одного ввода:-

Arrays.sort(contests, (row1, row2) -> {
            if (row1[0] < row2[0]) {
                return 1;
            } else return -1;
        });

Ошибка:-

Exception in thread "main" java.lang.IllegalArgumentException: Comparison method violates its general contract!
    at java.base/java.util.TimSort.mergeHi(TimSort.java:903)
    at java.base/java.util.TimSort.mergeAt(TimSort.java:520)
    at java.base/java.util.TimSort.mergeForceCollapse(TimSort.java:461)
    at java.base/java.util.TimSort.sort(TimSort.java:254)
    at java.base/java.util.Arrays.sort(Arrays.java:1441)
    at com.hackerrank.Solution.luckBalance(Solution.java:15)
    at com.hackerrank.Solution.main(Solution.java:49)

Глядя на ответы выше, я попытался добавить условие для equalsи я не знаю почему, но это сработало. Надеюсь, мы должны явно указать, что должно возвращаться для всех случаев (больше, равно и меньше):

        Arrays.sort(contests, (row1, row2) -> {
            if (row1[0] < row2[0]) {
                return 1;
            }
            if(row1[0] == row2[0]) return 0;
            return -1;
        });

Вариант ответа Гили, чтобы проверить, удовлетворяет ли компаратор требованиям, описанным в javadoc метода сравнения, с упором на полноту и удобочитаемость, например, называя переменные так же, как в javadoc. Обратите внимание, что это O(n^3), используйте его только при отладке, возможно, только для подмножества ваших элементов, чтобы быть достаточно быстрым, чтобы закончить вообще.

        public static <T> void verifyComparator(Comparator<T> comparator, Collection<T> elements) {
    for (T x : elements) {
      for (T y : elements) {
        for (T z : elements) {
          int x_y = comparator.compare(x, y);
          int y_x = comparator.compare(y, x);
          int y_z = comparator.compare(y, z);
          int x_z = comparator.compare(x, z);

          // javadoc: The implementor must ensure that sgn(compare(x, y)) == -sgn(compare(y, x))
          if (Math.signum(x_y) == -Math.signum(y_x)) { // ok
          } else {
            System.err.println("not holding: sgn(compare(x, y)) == -sgn(compare(y, x))" //
                + " | x_y: " + x_y + ", y_x: " + y_x + ",  x: " + x + ", y: " + y);
          }

          // javadoc: The implementor must also ensure that the relation is transitive:
          // ((compare(x, y)>0) && (compare(y, z)>0)) implies compare(x, z)>0.
          if (x_y > 0 && y_z > 0) {
            if (x_z > 0) { // ok
            } else {
              System.err.println("not holding: ((compare(x, y)>0) && (compare(y, z)>0)) implies compare(x, z)>0" //
                  + " | x_y: " + x_y + ", y_z: " + y_z + ",  x_z: " + x_z + ", x: " + x + ", y: " + y + ", z: " + z);
            }
          }

          // javadoc: Finally, the implementor must ensure that:
          // compare(x, y)==0 implies that sgn(compare(x, z))==sgn(compare(y, z)) for all z.
          if (x_y == 0) {
            if (Math.signum(x_z) == Math.signum(y_z)) { // ok
            } else {
              System.err.println("not holding: compare(x, y)==0 implies that sgn(compare(x, z))==sgn(compare(y, z)) for all z" //
                  + " | x_y: " + x_y + ", x_z: " + x_z + ",  y_z: " + y_z + ", x: " + x + ", y: " + y + ", z: " + z);
            }
          }
        }
      }
    }
  }

Как насчет того, чтобы сделать что-то более простое:

      int result = card1.getSet().compareTo(card2.getSet())
if (result == 0) {
    result = card1.getRarity().compareTo(card2.getRarity())
}
if (result == 0) {
    result = card1.getId().compareTo(card2.getId())
}
if (result == 0) {
    result = card1.getCardType().compareTo(card2.getCardType())
}
return result;

Вам просто нужно заказать сравнения в порядке предпочтения.

Пришлось сортировать по нескольким критериям (дата, и, если та же дата; другие вещи...). То, что работало на Eclipse с более старой версией Java, больше не работало на Android: метод сравнения нарушает контракт...

После прочтения Stackru я написал отдельную функцию, которую я вызывал из Compare(), если даты совпадают. Эта функция вычисляет приоритет в соответствии с критериями и возвращает -1, 0 или 1 для сравнения (). Кажется, сейчас работает.

Другие вопросы по тегам