Ошибка 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) ...
И это мой компаратор:
- Сравнение строк в cocoa
- Инструмент для сравнения большого количества файлов PDF?
- Ассемблер x86: сравнение с плавающей запятой
- Как сравнить один образ с другим, чтобы увидеть, похожи ли они на определенный процент, на iPhone?
- Условие запроса MongoDb при сравнении двух полей
@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() item.getCardType()) { return 1; } else { if (cardType == item.getCardType()) { return 0; } return -1; } } return -1; } } return 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!
Это также имеет отношение к версии JDK. Если это хорошо в JDK6, возможно, проблема будет в JDK 7, описанная вами, потому что был изменен метод реализации в jdk 7.
Посмотри на это:
Описание: Алгоритм сортировки, используемый java.util.Arrays.sort
и (косвенно) с помощью java.util.Collections.sort
, был заменен. Новая реализация sort может IllegalArgumentException
если оно обнаруживает Comparable
которое нарушает договор Comparable
. Предыдущая реализация молча игнорировала такую ситуацию. Если требуется предыдущее поведение, вы можете использовать новое системное свойство java.util.Arrays.useLegacyMergeSort
, чтобы восстановить предыдущее поведение слияния.
Я не знаю точной причины. Однако, если вы добавите код, прежде чем использовать сортировку. Все будет хорошо.
System.setProperty("java.util.Arrays.useLegacyMergeSort", "true");
Вы можете использовать следующий class, чтобы определить ошибки транзитивности в ваших компараторах:
/** * @author Gili Tzabari */ public final class Comparators { /** * Verify that a comparator is transitive. * * @param 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 void verifyTransitivity(Comparator comparator, Collection 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)
перед кодом, который терпит неудачу.
Рассмотрим следующий случай:
Сначала o1.compareTo(o2)
. card1.getSet() == card2.getSet()
случается, является истинным, и поэтому card1.getRarity() < card2.getRarity()
, поэтому вы возвращаете 1.
Затем o2.compareTo(o1)
. Опять же, card1.getSet() == card2.getSet()
- true. Затем вы переходите к следующему, а затем card1.getId() == card2.getId()
имеет значение true, а также cardType > item.getCardType()
. Вы снова возвращаетесь 1.
Отсюда следует, что o1 > o2
и o2 > o1
. Вы нарушили контракт.
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; } }
Мне пришлось сортировать по нескольким критериям (дата и, если такая же дата, другие вещи …). То, что работало на Eclipse со старой версией Java, больше не работало на Android: метод сравнения нарушает контракт …
После чтения в StackOverflow я написал отдельную функцию, которую я вызвал из compare (), если даты совпадают. Эта функция вычисляет приоритет в соответствии с критериями и возвращает -1, 0 или 1 для сравнения (). Кажется, теперь это работает.
Я получил ту же ошибку с classом, что и следующий StockPickBean
. Вызывается из этого кода:
List beansListcatMap.getValue(); beansList.sort(StockPickBean.Comparators.VALUE); public class StockPickBean implements Comparable { 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 VALUE = (val1, val2) -> (int) (val1.value - val2.value); } }
После получения той же ошибки:
java.lang.IllegalArgumentException: метод сравнения нарушает общий контракт!
Я изменил эту строку:
public static Comparator VALUE = (val1, val2) -> (int) (val1.value - val2.value);
чтобы:
public static Comparator VALUE = (StockPickBean spb1, StockPickBean spb2) -> Double.compare(spb2.value,spb1.value);
Это исправляет ошибку.