Java-Fehler: Die Vergleichsmethode verstößt gegen den allgemeinen Vertrag

84

Ich habe viele Fragen dazu gesehen und versucht, das Problem zu lösen, aber nach einer Stunde Googeln und viel Ausprobieren kann ich es immer noch nicht beheben. Ich hoffe, einige von Ihnen haben das Problem erkannt.

Das bekomme ich:

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)
    ...

Und das ist mein Komparator:

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

Irgendeine Idee?

Lakatos Gyula
quelle
Welche Codezeile bewirkt, dass diese Ausnahme ausgelöst wird? Was steht in den Zeilen 835 und 453 von ComparableTimSort.java?
Luftkissenfahrzeug voller Aale
Es scheint mir, dass Sie diese Methode an die delegieren sollten Card Klasse : return card1.compareTo(card2)und diese Logik dort implementieren sollten.
Hunter McMillen
2
@HovercraftFullOfEels Es ist eine Klasse von Oracle, die nicht von mir geschrieben wurde. Es gibt eine Ausnahme in dieser Zeile. Die Methode ist sehr lang und sieht schwer zu verstehen aus.
Lakatos Gyula
@ HunterMcMillen Das kann ich nicht. Die Karte enthält nur die statischen Daten einer Karte (Name, Text usw.), während diese Klasse einige sich ändernde Variablen wie Typ und Menge enthält.
Lakatos Gyula
1
Nachdem ich das Clean Code-Buch gelesen habe, weiß ich es auch nicht.
Lakatos Gyula

Antworten:

97

Die Ausnahmemeldung ist eigentlich ziemlich beschreibend. Der Vertrag, den es erwähnt, ist Transitivität : wenn A > Bund B > Cdann für irgendeinen A, Bund C: A > C. Ich habe es mit Papier und Bleistift überprüft und Ihr Code scheint einige Löcher zu haben:

if (card1.getRarity() < card2.getRarity()) {
  return 1;

Sie kehren nicht zurück, -1wenn card1.getRarity() > card2.getRarity().


if (card1.getId() == card2.getId()) {
  //...
}
return -1;

Sie kehren zurück, -1wenn die IDs nicht gleich sind. Sie sollten zurückkehren -1oder 1je nachdem, welche ID größer war.


Schau dir das an. Abgesehen davon, dass es viel besser lesbar ist, denke ich, dass es tatsächlich funktionieren sollte:

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!
Tomasz Nurkiewicz
quelle
15
Tatsächlich verletzt das von Ihnen angegebene Beispiel nicht die Transitivität, sondern die Regel sgn (vergleiche (x, y)) == -sgn (vergleiche (y, x)), wie in docs.oracle.com/javase/7/docs/ angegeben. api / java / util /… - das ist mathematisch gesehen Antisymmetrie.
Hans-Peter Störr
1
Ich würde vorschlagen, if (card1.getSet() != card2.getSet()) return card1.getSet() > card2.getSet() ? 1 : -1;für mehr Geschwindigkeit zu verwenden, wenn es jemanden interessiert.
Maaartinus
Ich bin darauf gestoßen und es war ein Symmetrieproblem in meinem Komparator. Der schwer zu diagnostizierende Teil war, dass meine Schwäche das erste Feld war (eine boolesche Feldprüfung, kein richtiger Vergleich), das nicht prüfte, ob beide richtig waren. Dies wurde jedoch erst aufgedeckt, nachdem ich einen nachfolgenden untergeordneten Vergleich hinzugefügt hatte, der die Sortierreihenfolge gestört haben muss.
Thomas W
1
@maaartinus danke für deinen Vorschlag! Es war perfekt für mich :)
Sonhja
45

Sie können die folgende Klasse verwenden, um Transitivitätsfehler in Ihren Komparatoren zu lokalisieren:

/**
 * @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()
    {
    }
}

Rufen Sie einfach vor dem fehlgeschlagenen Comparators.verifyTransitivity(myComparator, myCollection)Code auf.

Gili
quelle
3
Dieser Code validiert compare (a, b) = - compare (b, c). Es wird nicht überprüft, ob, wenn vergleiche (a, b)> 0 && vergleiche (b, c)> 0, dann vergleiche (a, c)> 0
Olaf Achthoven
3
Ich fand deine Klasse wirklich sehr, sehr nützlich. Vielen Dank.
Crigore
Vielen Dank, ich musste zurückkommen und die Antwort positiv bewerten, da diese Klasse beim Debuggen eines Komparators sehr nützlich ist, nicht nur für den hier genannten Zweck, da sie geändert werden kann, um auch andere Umstände zu testen!
Jaco-Ben Vosloo
36

Es hat auch etwas mit der Version von JDK zu tun. Wenn es in JDK6 gut funktioniert, liegt möglicherweise das von Ihnen beschriebene Problem in JDK 7 vor, da die Implementierungsmethode in JDK 7 geändert wurde.

Schau dir das an:

Beschreibung: Der von java.util.Arrays.sortund (indirekt) von verwendete Sortieralgorithmus java.util.Collections.sortwurde ersetzt. Die neue Sortierimplementierung kann eine IllegalArgumentExceptionauslösen Comparable, wenn sie eine erkennt , die den ComparableVertrag verletzt . Die vorherige Implementierung ignorierte eine solche Situation stillschweigend. Wenn das vorherige Verhalten gewünscht wird, können Sie die neue Systemeigenschaft verwenden java.util.Arrays.useLegacyMergeSort, um das vorherige Mergesort-Verhalten wiederherzustellen.

Ich kenne den genauen Grund nicht. Wenn Sie jedoch den Code hinzufügen, bevor Sie sort verwenden. Es wird in Ordnung sein.

System.setProperty("java.util.Arrays.useLegacyMergeSort", "true");
Justin Civi
quelle
1
Das Problem lag in der Logik, mit der ich Dinge verglichen habe. Ich werde dies positiv bewerten, hoffentlich kann es jemandem helfen, der nach dieser Art von Problem sucht.
Lakatos Gyula
10
Dies sollte nur als schnelle und hässliche Problemumgehung verwendet werden, damit die Dinge wieder funktionieren, bis Sie den Komparator reparieren. Wenn die Ausnahme auftritt, bedeutet dies, dass Ihr Komparator fehlerhaft ist und die Sortierreihenfolge seltsam ist. Sie müssen alle in docs.oracle.com/javase/7/docs/api/java/util/… angegebenen Regeln berücksichtigen .
Hans-Peter Störr
Was ist, wenn "komisch" das ist, was ich wollte? (a,b) -> { return (new int[]{-1,1})[(int)((Math.random()*2)%2)]}Ich weiß Collections.shuffle, dass es existiert, aber ich mag es nicht, übermäßig geschützt zu sein.
Jefferey Cave
Ich habe eine alte Anwendung und mit JDK8 habe ich diesen Fehler bekommen. Mit JDK6 funktioniert es richtig, also rüste ich einfach auf 6 herunter, danke.
Stefano Scarpanti
6

Betrachten Sie den folgenden Fall:

Zunächst o1.compareTo(o2)heißt. card1.getSet() == card2.getSet()zufällig wahr und so istcard1.getRarity() < card2.getRarity() , also geben Sie 1 zurück.

Dann o2.compareTo(o1)heißt es. Wieder card1.getSet() == card2.getSet()ist wahr. Dann springen Sie zum Folgenden else, dann ist card1.getId() == card2.getId()es wahr, und so ist es auchcardType > item.getCardType() . Sie geben wieder 1 zurück.

Daraus o1 > o2und o2 > o1. Sie haben den Vertrag gebrochen.

eran
quelle
2
        if (card1.getRarity() < card2.getRarity()) {
            return 1;

Wenn card2.getRarity()jedoch kleiner als ist, geben card1.getRarity()Sie möglicherweise nicht -1 zurück .

Sie vermissen auch andere Fälle. Ich würde das tun, Sie können sich je nach Ihrer Absicht ändern:

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;

    }
}
Joe
quelle
1

Es könnte auch ein OpenJDK-Fehler sein ... (nicht in diesem Fall, aber es ist der gleiche Fehler)

Wenn jemand wie ich über diese Antwort bezüglich der stolpert

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

dann könnte es auch ein Fehler in der Java-Version sein. Ich habe einen Vergleich, der seit einigen Jahren in einigen Anwendungen ausgeführt wird. Aber plötzlich hat es aufgehört zu funktionieren und wirft den Fehler aus, nachdem alle Vergleiche durchgeführt wurden (ich vergleiche 6 Attribute, bevor ich "0" zurückgebe).

Jetzt habe ich gerade diesen Bugreport von OpenJDK gefunden:

Leole
quelle
1

Ich hatte das gleiche Symptom. Für mich stellte sich heraus, dass ein anderer Thread die verglichenen Objekte modifizierte, während die Sortierung in einem Stream stattfand. Um das Problem zu beheben, habe ich die Objekte unveränderlichen temporären Objekten zugeordnet, den Stream einer temporären Sammlung zugeordnet und danach sortiert.

Carl Rosenberger
quelle
0

Ich musste nach mehreren Kriterien sortieren (Datum und, wenn dasselbe Datum; andere Dinge ...). Was mit einer älteren Java-Version an Eclipse funktionierte, funktionierte unter Android nicht mehr: Vergleichsmethode verstößt gegen Vertrag ...

Nach dem Lesen von StackOverflow habe ich eine separate Funktion geschrieben, die ich von compare () aufgerufen habe, wenn die Daten identisch sind. Diese Funktion berechnet die Priorität gemäß den Kriterien und gibt -1, 0 oder 1 zurück, um sie zu vergleichen (). Es scheint jetzt zu funktionieren.

Jean Burkhardt
quelle
0

Ich habe den gleichen Fehler mit einer Klasse wie der folgenden erhalten StockPickBean. Von diesem Code aufgerufen:

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

Nach dem gleichen Fehler:

java.lang.IllegalArgumentException: Die Vergleichsmethode verstößt gegen ihren allgemeinen Vertrag!

Ich habe diese Zeile geändert:

public static Comparator<StockPickBean> VALUE = (val1, val2) -> (int) 
         (val1.value - val2.value);

zu:

public static Comparator<StockPickBean> VALUE = (StockPickBean spb1, 
StockPickBean spb2) -> Double.compare(spb2.value,spb1.value);

Das behebt den Fehler.

pmkent
quelle
0

Ich bin auf ein ähnliches Problem gestoßen, bei dem ich versucht habe, einen n x 2 2D arrayNamen zu sortieren, bei contestsdem es sich um ein 2D-Array einfacher Ganzzahlen handelt. Dies funktionierte die meiste Zeit, warf jedoch einen Laufzeitfehler für eine Eingabe aus: -

Arrays.sort(contests, (row1, row2) -> {
            if (row1[0] < row2[0]) {
                return 1;
            } else return -1;
        });

Error:-

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)

Mit Blick auf die obigen Antworten habe ich versucht, eine Bedingung für hinzuzufügen, equalsund ich weiß nicht warum, aber es hat funktioniert. Hoffentlich müssen wir explizit angeben, was für alle Fälle zurückgegeben werden soll (größer als, gleich und kleiner als):

        Arrays.sort(contests, (row1, row2) -> {
            if (row1[0] < row2[0]) {
                return 1;
            }
            if(row1[0] == row2[0]) return 0;
            return -1;
        });
Prateek Bhuwania
quelle