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