Помилка Java: метод порівняння порушує загальний контракт


84

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

Ось що я отримую:

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;
    }
}

Будь-яка ідея?


Який рядок коду спричиняє виникнення цього винятку? Що є у рядках 835 та 453 порівняльногоTimSort.java?
Судно на повітряній подушці, повне вугрів,

Мені здається, що ви повинні мати цей метод делегувати Cardкласу: return card1.compareTo(card2)і впровадити цю логіку там.
Хантер Макміллен,

2
@HovercraftFullOfEels Це клас від Oracle, не розлючений мною. Це виключення на цьому напрямі. Метод дуже довгий і виглядає важко зрозуміти.
Lakatos Gyula

@HunterMcMillen Я не можу цього зробити. Картка містить лише статичні дані картки (ім'я, текст тощо ...), тоді як цей клас містить деякі змінні змінні, такі як тип і кількість.
Lakatos Gyula

1
Після прочитання книги «Чистий код» я теж не знаю.
Lakatos Gyula

Відповіді:


97

Повідомлення про виняток насправді є досить описовим. У контракті він згадує це транзитивність : якщо 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!

15
Насправді, наведений вами приклад не порушує транзитивності, але правило sgn (порівняння (x, y)) == -sgn (порівняння (y, x)), як зазначено в docs.oracle.com/javase/7/docs/ api / java / util /… - це антисиметрія в математичному плані.
Hans-Peter Störr

1
Я б запропонував використовувати if (card1.getSet() != card2.getSet()) return card1.getSet() > card2.getSet() ? 1 : -1;для більшої швидкості, якщо когось це цікавить.
maaartinus

Я зіткнувся з цим, і це було проблемою симетрії в моєму компараторі. Важкою частиною діагностики було те, що моєю слабкістю було перше поле (логічна перевірка поля, а не належне порівняння), яке не перевіряло, чи є обидва істинними правильними. Однак це було викрито лише після того, як я додав наступне підлегле порівняння, яке, мабуть, порушило порядок сортування.
Thomas W

1
@maaartinus дякую за вашу пропозицію! Для мене це було ідеально :)
Sonhja

45

Ви можете використовувати наступний клас, щоб визначити помилки транзитивності у своїх компараторах:

/**
 * @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
Цей код перевіряє порівняння (a, b) = - порівняння (b, c). Це не перевіряє, якщо порівняти (a, b)> 0 && порівняти (b, c)> 0, то порівняти (a, c)> 0
Олаф Ахтовен

3
Я дійсно знайшов твій клас дуже, дуже корисним. Дякую.
crigore

Дякую, мені довелося повернутися і проголосувати відповідь, оскільки цей клас дуже корисний при налагодженні компаратора, а не лише для зазначеної тут мети, оскільки його можна змінити для перевірки інших обставин!
Жако-Бен Восло

36

Це також має щось спільне з версією 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");

1
Проблема полягала в логіці, яку я використовував для порівняння речей. Я підтримаю це, сподіваюся, це може допомогти тому, хто шукає такий тип проблем.
Lakatos Gyula

10
Це слід використовувати лише як швидке та потворне рішення, щоб змусити речі знову працювати, поки ви не виправите компаратор. Якщо трапиться виняток, це означає, що у компараторі є помилка, і порядок сортування буде дивним. Ви повинні врахувати всі правила, наведені в docs.oracle.com/javase/7/docs/api/java/util/… .
Hans-Peter Störr

Що, якщо "дивний" - це те, на що я йшов? (a,b) -> { return (new int[]{-1,1})[(int)((Math.random()*2)%2)]}Я знаю, що Collections.shuffleіснує, але мені не подобається бути надмірно захищеним.
Печера Джеффрі

У мене є стара програма, і з JDK8 я отримав цю помилку. Використовуючи JDK6, він працює правильно, тому я просто знижую його до 6, дякую.
Стефано Скарпанті

6

Розглянемо наступний випадок:

По-перше, 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. Ви розірвали контракт.


2
        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;

    }
}

1

Це також може бути помилка OpenJDK ... (не в цьому випадку, але це та сама помилка)

Якщо хтось на зразок мене натрапить на цю відповідь щодо

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

тоді це також може бути помилкою у Java-версії. У мене є порівняння, яке працює вже кілька років у деяких додатках. Але раптом він перестав працювати і видає помилку після того, як були проведені всі порівняння (я порівнюю 6 атрибутів, перш ніж повернути "0").

Зараз я щойно знайшов цей Bugreport OpenJDK:


1

У мене був такий самий симптом. Для мене виявилося, що інший потік модифікував порівнювані об'єкти під час сортування в потоці. Щоб вирішити проблему, я зіставив об'єкти на незмінні тимчасові об'єкти, зібрав Потік до тимчасової колекції та здійснив сортування за цим.


0

Мені довелося сортувати за кількома критеріями (дата, і, якщо одна дата; інші речі ...). Те, що працювало на Eclipse зі старою версією Java, більше не працювало на Android: метод порівняння порушує контракт ...

Прочитавши StackOverflow, я написав окрему функцію, яку я викликав з порівняння (), якщо дати однакові. Ця функція обчислює пріоритет відповідно до критеріїв і повертає -1, 0 або 1 для порівняння (). Здається, це працює зараз.


0

Я отримав ту ж помилку з класом, як показано нижче 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);

Це виправляє помилку.


0

Я зіткнувся з подібною проблемою, коли намагався відсортувати 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;
        });
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.