Java-fout: vergelijkingsmethode schendt het algemene contract

Ik zag hier veel vragen over en probeerde het probleem op te lossen, maar na een uur googlen en veel proberen & fout, ik kan het nog steeds niet oplossen. Ik hoop dat sommigen van jullie het probleem begrijpen.

Dit is wat ik krijg:

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

En dit is mijn vergelijker:

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

Enig idee?


Antwoord 1, autoriteit 100%

Het uitzonderingsbericht is eigenlijk vrij beschrijvend. Het contract dat het vermeldt is transitiviteit: if A > Ben B > Cdan voor elke A, Ben C: A > C. Ik heb het nagekeken met papier en potlood en je code lijkt weinig gaten te hebben:

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

u retourneert -1niet als card1.getRarity() > card2.getRarity().


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

Je retourneert -1als id’s niet gelijk zijn. Je moet -1of 1retourneren, afhankelijk van welke id groter was.


Kijk hier eens naar. Behalve dat het veel leesbaarder is, denk ik dat het ook echt zou moeten werken:

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!

Antwoord 2, autoriteit 45%

U kunt de volgende klasse gebruiken om transitiviteitsbugs in uw Comparators op te sporen:

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

Vraag gewoon Comparators.verifyTransitivity(myComparator, myCollection)aan voor de code die mislukt.


Antwoord 3, autoriteit 36%

Het heeft ook iets te maken met de versie van JDK.
Als het het goed doet in JDK6, heeft het misschien het door u beschreven probleem in JDK 7, omdat de implementatiemethode in jdk 7 is gewijzigd.

Kijk hier eens naar:

Beschrijving: het sorteeralgoritme dat wordt gebruikt door java.util.Arrays.sorten (indirect) door java.util.Collections.sortis vervangen. De nieuwe sorteerimplementatie kan een IllegalArgumentExceptiongenereren als het een Comparabledetecteert dat het Comparable-contract schendt. De vorige implementatie negeerde een dergelijke situatie stilzwijgend. Als het vorige gedrag gewenst is, kunt u de nieuwe systeemeigenschap, java.util.Arrays.useLegacyMergeSort, gebruiken om eerder mergesort-gedrag te herstellen.

Ik weet de exacte reden niet. Als u de code echter toevoegt voordat u sort. Het komt goed.

System.setProperty("java.util.Arrays.useLegacyMergeSort", "true");

Antwoord 4, autoriteit 6%

Beschouw het volgende geval:

Eerst wordt o1.compareTo(o2)aangeroepen. card1.getSet() == card2.getSet()is toevallig waar en card1.getRarity() < card2.getRarity(), dus je retourneert 1.

Vervolgens wordt o2.compareTo(o1)aangeroepen. Nogmaals, card1.getSet() == card2.getSet()is waar. Dan ga je naar het volgende else, dan is card1.getId() == card2.getId()waar, en dat geldt ook voor cardType > item.getCardType(). Je geeft er weer 1 terug.

Daaruit, o1 > o2, en o2 > o1. Je hebt het contract verbroken.


Antwoord 5, autoriteit 2%

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

Als card2.getRarity()echter kleiner is dan card1.getRarity(), retourneert u mogelijk geen -1.

Je mist op dezelfde manier andere zaken. Ik zou dit doen, je kunt veranderen, afhankelijk van je intentie:

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

Antwoord 6, autoriteit 2%

Het kan ook een OpenJDK-bug zijn… (in dit geval niet, maar het is dezelfde fout)

Als iemand zoals ik dit antwoord tegenkomt met betrekking tot de

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

dan kan het ook een bug in de Java-versie zijn. Ik heb een CompareTo die sinds enkele jaren in sommige toepassingen loopt. Maar plotseling stopte het met werken en gaf de fout nadat alle vergelijkingen waren gedaan (ik vergelijk 6 attributen voordat ik “0” retourneerde).

Nu heb ik dit bugrapport van OpenJDK gevonden:


Antwoord 7

Ik had hetzelfde symptoom. Voor mij bleek dat een andere thread de vergeleken objecten aan het aanpassen was terwijl het sorteren in een Stream plaatsvond. Om het probleem op te lossen, heb ik de objecten toegewezen aan onveranderlijke tijdelijke objecten, de stream verzameld in een tijdelijke verzameling en daarop gesorteerd.


Antwoord 8

Ik moest op verschillende criteria sorteren (datum, en, indien dezelfde datum; andere dingen…). Wat werkte op Eclipse met een oudere versie van Java, werkte niet meer op Android: vergelijkingsmethode schendt contract …

Na het lezen op StackOverflow, heb ik een aparte functie geschreven die ik heb aangeroepen vanuit Compare() als de datums hetzelfde zijn. Deze functie berekent de prioriteit volgens de criteria en retourneert -1, 0 of 1 om te vergelijken(). Het lijkt nu te werken.


Antwoord 9

Ik kreeg dezelfde fout met een klasse zoals de volgende StockPickBean. Geroepen vanaf deze code:

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

Na dezelfde foutmelding:

java.lang.IllegalArgumentException: vergelijkingsmethode schendt het algemene contract!

Ik heb deze regel gewijzigd:

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

naar:

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

Dat lost de fout op.


Antwoord 10

Ik kwam een soortgelijk probleem tegen waarbij ik probeerde een n x 2 2D arraygenaamd contestste sorteren, wat een 2D-array van eenvoudige gehele getallen is. Dit werkte meestal, maar gaf een runtime-fout voor één invoer:-

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

Fout:-

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)

Kijkend naar de antwoorden hierboven heb ik geprobeerd een voorwaarde toe te voegen voor equalsen ik weet niet waarom, maar het werkte. Hopelijk moeten we expliciet specificeren wat er voor alle gevallen moet worden geretourneerd (groter dan, gelijk aan en kleiner dan):

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

Other episodes