Ошибка 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;
    }
}

Любая идея?

Ответ 1

Сообщение об исключении на самом деле довольно наглядное. Контракт, который он упоминает, является транзитивностью: если 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!

Ответ 2

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

/**
 * @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) перед ошибкой кода.

Ответ 3

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

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

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

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

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

Ответ 4

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

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

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

Из этого, o1 > o2 и o2 > o1. Вы нарушили контракт.

Ответ 5

        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;

    }
}

Ответ 6

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

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

Ответ 7

Я получил ту же ошибку с классом, подобным следующему 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);

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

Ответ 8

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

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

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

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

Теперь я только что нашел этот багрепорт OpenJDK: