199

Can someone explain me in simple terms, why does this code throw an exception, "Comparison method violates its general contract!", and how do I fix it?

private int compareParents(Foo s1, Foo s2) {
    if (s1.getParent() == s2) return -1;
    if (s2.getParent() == s1) return 1;
    return 0;
}
Stuart Marks
  • 112,017
  • 32
  • 182
  • 245
n00bster
  • 2,305
  • 2
  • 14
  • 15
  • And the exception that is thrown as well. – Matthew Farwell Nov 30 '11 at 14:36
  • 2
    I don't know much about Java or about Java comparison APIs, but this comparison method seems dead wrong. Suppose `s1` is the parent of `s2`, and `s2` is not the parent of `s1`. Then `compareParents(s1, s2)` is `0`, but `compareParents(s2, s1)` is `1`. That doesn't make sense. (In addition, it's not transitive, like aix mentioned below.) – mqp Nov 30 '11 at 14:38
  • 4
    This error appears to only be produced by a specific library http://cr.openjdk.java.net/~martin/webrevs/openjdk7/timsort/src/share/classes/java/util/TimSort.java.html – Peter Lawrey Nov 30 '11 at 14:40
  • in java, you can use equals (return a boolean) or compareTo (return -1, 0 or +1). Override this functions in your Foo class and after that, you can check s1.getParent().equals(s2) ... – Mualig Nov 30 '11 at 14:42
  • 1
    What is the name and class of the Exception? Is it an IllegalArgumentException? If I had to guess I would think that you should be doing `s1.getParent().equals(s2)` instead of `s1.getParent() == s2`. – Freiheit Nov 30 '11 at 14:35
  • Whether or not the exception is thrown depends on the used JRE version. Java6 will allow this, Java 7 and 8 will throw the error. – THelper Apr 05 '15 at 12:57

12 Answers12

274

Your comparator is not transitive.

Let A be the parent of B, and B be the parent of C. Since A > B and B > C, then it must be the case that A > C. However, if your comparator is invoked on A and C, it would return zero, meaning A == C. This violates the contract and hence throws the exception.

It's rather nice of the library to detect this and let you know, rather than behave erratically.

One way to satisfy the transitivity requirement in compareParents() is to traverse the getParent() chain instead of only looking at the immediate ancestor.

rkg
  • 165
  • 3
  • 13
NPE
  • 438,426
  • 93
  • 887
  • 970
40

Just because this is what I got when I Googled this error, my problem was that I had

if (value < other.value)
  return -1;
else if (value >= other.value)
  return 1;
else
  return 0;

the value >= other.value should (obviously) actually be value > other.value so that you can actually return 0 with equal objects.

you786
  • 3,735
  • 4
  • 42
  • 67
  • 8
    I have to add that if any of your `value` is a NaN (if `value` is a `double` or `float`), it would fail as well. – Matthieu Apr 11 '14 at 18:44
25

The violation of the contract often means that the comparator is not providing the correct or consistent value when comparing objects. For example, you might want to perform a string compare and force empty strings to sort to the end with:

if ( one.length() == 0 ) {
    return 1;                   // empty string sorts last
}
if ( two.length() == 0 ) {
    return -1;                  // empty string sorts last                  
}
return one.compareToIgnoreCase( two );

But this overlooks the case where BOTH one and two are empty - and in that case, the wrong value is returned (1 instead of 0 to show a match), and the comparator reports that as a violation. It should have been written as:

if ( one.length() == 0 ) {
    if ( two.length() == 0 ) {
        return 0;               // BOth empty - so indicate
    }
    return 1;                   // empty string sorts last
}
if ( two.length() == 0 ) {
    return -1;                  // empty string sorts last                  
}
return one.compareToIgnoreCase( two );
CESDewar
  • 361
  • 3
  • 4
14

Even if your compareTo is holds transitivity in theory, sometimes subtle bugs mess things up... such as floating point arithmetic error. It happened to me. this was my code:

public int compareTo(tfidfContainer compareTfidf) {
    //descending order
    if (this.tfidf > compareTfidf.tfidf)
        return -1;
    else if (this.tfidf < compareTfidf.tfidf)
        return 1;
    else
        return 0;

}   

The transitive property clearly holds, but for some reason I was getting the IllegalArgumentException. And it turns out that due to tiny errors in floating point arithmetic, the round-off errors where causing the transitive property to break where they shouldn't! So I rewrote the code to consider really tiny differences 0, and it worked:

public int compareTo(tfidfContainer compareTfidf) {
    //descending order
    if ((this.tfidf - compareTfidf.tfidf) < .000000001)
        return 0;
    if (this.tfidf > compareTfidf.tfidf)
        return -1;
    else if (this.tfidf < compareTfidf.tfidf)
        return 1;
    return 0;
}   
SomeGuy
  • 959
  • 10
  • 13
  • 2
    This was helpful! My code was logically okay but there was an error because of the precision problem. – JSong Sep 24 '18 at 03:26
7

In our case were were getting this error because we had accidentally flipped the order of comparison of s1 and s2. So watch out for that. It was obviously way more complicated than the following but this is an illustration:

s1 == s2   
    return 0;
s2 > s1 
    return 1;
s1 < s2 
    return -1;
Uncle Iroh
  • 4,961
  • 4
  • 43
  • 55
4

In my case I was doing something like the following:

if (a.someField == null) {
    return 1;
}

if (b.someField == null) {
    return -1;
}

if (a.someField.equals(b.someField)) {
    return a.someOtherField.compareTo(b.someOtherField);
}

return a.someField.compareTo(b.someField);

What I forgot to check was when both a.someField and b.someField are null.

jc12
  • 893
  • 11
  • 22
3

I've seen this happen in a piece of code where the often recurring check for null values was performed:

if(( A==null ) && ( B==null )
  return +1;//WRONG: two null values should return 0!!!
Draken
  • 3,049
  • 13
  • 32
  • 49
keesp
  • 173
  • 11
3

Java does not check consistency in a strict sense, only notifies you if it runs into serious trouble. Also it does not give you much information from the error.

I was puzzled with what's happening in my sorter and made a strict consistencyChecker, maybe this will help you:

/**
 * @param dailyReports
 * @param comparator
 */
public static <T> void checkConsitency(final List<T> dailyReports, final Comparator<T> comparator) {
  final Map<T, List<T>> objectMapSmallerOnes = new HashMap<T, List<T>>();

  iterateDistinctPairs(dailyReports.iterator(), new IPairIteratorCallback<T>() {
    /**
     * @param o1
     * @param o2
     */
    @Override
    public void pair(T o1, T o2) {
      final int diff = comparator.compare(o1, o2);
      if (diff < Compare.EQUAL) {
        checkConsistency(objectMapSmallerOnes, o1, o2);
        getListSafely(objectMapSmallerOnes, o2).add(o1);
      } else if (Compare.EQUAL < diff) {
        checkConsistency(objectMapSmallerOnes, o2, o1);
        getListSafely(objectMapSmallerOnes, o1).add(o2);
      } else {
        throw new IllegalStateException("Equals not expected?");
      }
    }
  });
}

/**
 * @param objectMapSmallerOnes
 * @param o1
 * @param o2
 */
static <T> void checkConsistency(final Map<T, List<T>> objectMapSmallerOnes, T o1, T o2) {
  final List<T> smallerThan = objectMapSmallerOnes.get(o1);

  if (smallerThan != null) {
    for (final T o : smallerThan) {
      if (o == o2) {
        throw new IllegalStateException(o2 + "  cannot be smaller than " + o1 + " if it's supposed to be vice versa.");
      }
      checkConsistency(objectMapSmallerOnes, o, o2);
    }
  }
}

/**
 * @param keyMapValues 
 * @param key 
 * @param <Key> 
 * @param <Value> 
 * @return List<Value>
 */ 
public static <Key, Value> List<Value> getListSafely(Map<Key, List<Value>> keyMapValues, Key key) {
  List<Value> values = keyMapValues.get(key);

  if (values == null) {
    keyMapValues.put(key, values = new LinkedList<Value>());
  }

  return values;
}

/**
 * @author Oku
 *
 * @param <T>
 */
public interface IPairIteratorCallback<T> {
  /**
   * @param o1
   * @param o2
   */
  void pair(T o1, T o2);
}

/**
 * 
 * Iterates through each distinct unordered pair formed by the elements of a given iterator
 *
 * @param it
 * @param callback
 */
public static <T> void iterateDistinctPairs(final Iterator<T> it, IPairIteratorCallback<T> callback) {
  List<T> list = Convert.toMinimumArrayList(new Iterable<T>() {

    @Override
    public Iterator<T> iterator() {
      return it;
    }

  });

  for (int outerIndex = 0; outerIndex < list.size() - 1; outerIndex++) {
    for (int innerIndex = outerIndex + 1; innerIndex < list.size(); innerIndex++) {
      callback.pair(list.get(outerIndex), list.get(innerIndex));
    }
  }
}
Martin
  • 1,285
  • 14
  • 20
  • Simply invoke checkConsitency metohod with parameter list and comparator. – Martin Mar 20 '15 at 04:25
  • Your code does not compile. Classes `Compare`, `Convert` (and potentially others) are not defined. Please update the code sniplet with a self-contained example. – Gili Jan 25 '16 at 19:05
  • You should fix the typo in `checkConsi(s)tency` and remove all redundant `@param` declarations to make the code more readable. – Roland Illig Feb 07 '17 at 13:35
3

Editing VM Configuration worked for me.

-Djava.util.Arrays.useLegacyMergeSort=true
Yunnosch
  • 21,438
  • 7
  • 35
  • 44
Rishav Roy
  • 51
  • 2
  • Please double check that my attempt to help you with the formatting did not break anything. I am unsure about the `-` a the start of the proposed solution. Maybe you intended something like a one-item bullet point list instead. – Yunnosch Jul 23 '19 at 17:18
  • 2
    Also please explain how this helps with the described problem. It currently is practically a code-only answer. – Yunnosch Jul 23 '19 at 17:19
2

If compareParents(s1, s2) == -1 then compareParents(s2, s1) == 1 is expected. With your code it's not always true.

Specifically if s1.getParent() == s2 && s2.getParent() == s1. It's just one of the possible problems.

Andrii Karaivanskyi
  • 1,730
  • 2
  • 18
  • 22
1

In my case, it was an infinite sort. That is, at first the line moved up according to the condition, and then the same line moved down to the same place. I added one more condition at the end that unambiguously established the order of the lines.

Chego
  • 123
  • 8
0

You can't compare object data like this:s1.getParent() == s2 - this will compare the object references. You should override equals function for Foo class and then compare them like this s1.getParent().equals(s2)

EkcenierK
  • 1,398
  • 1
  • 18
  • 34
erimerturk
  • 4,020
  • 22
  • 25