Ошибка 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:
- JDK-8210311
- Затрагивает версии / с: 8, 11
- Исправить версии / с: 12
- https://bugs.openjdk.java.net/browse/JDK-8210311
Происхождение этого исключения является неправильным
Comparator
реализация. Проверяя документы , мы должны реализовать
compare(o1, o2)
метод как отношение эквивалентности , следуя правилам:
- a.equals(b) — сравнение (a, b) —
0
- a.compare(b) > 0 b.compare(a) < 0
-
if
a.compare(b) > 0 и b.compare(c) > 0then
a.compare(c) > 0true
Вы можете проверить свой код, чтобы понять, где ваша реализация нарушает одно или несколько правил контракта 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 для сравнения (). Кажется, сейчас работает.