Java 7 changed the sorting algorithm such that it throws an
java.lang.IllegalArgumentException: "Comparison method violates its general contract!"
in some cases when the used comparator is buggy. Is it possible to tell what kind of bug in the comparator causes this? In my experiments it did not matter if x != x , it also did not matter if x < y and y < z but z < x , but it did matter if x = y and y = z but x < z for some values x, y, z. Is this generally so?
(If there were a general rule to this, it might be easier to look for the bug in the comparator. But of course it is better to fix all bugs. :-) )
In particular, the following two comparators did not make TimSort complain:
final Random rnd = new Random(52);
Comparator<Integer> brokenButNoProblem1 = new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
if (o1 < o2) {
return Compare.LESSER;
} else if (o1 > o2) {
return Compare.GREATER;
}
return rnd.nextBoolean() ? Compare.LESSER : Compare.GREATER;
}
};
Comparator<Integer> brokenButNoProblem2 = new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
if (o1 == o2) {
return Compare.EQUAL;
}
return rnd.nextBoolean() ? Compare.LESSER : Compare.GREATER;
}
};
but the following comparator did make it throw up:
Comparator<Integer> brokenAndThrowsUp = new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
if (Math.abs(o1 - o2) < 10) {
return Compare.EQUAL; // WRONG and does matter
}
return Ordering.natural().compare(o1, o2);
}
};
UPDATE: in some real life data we had a failure where there were no x,y,z with x = y and y = z but x < z . So It seems my guess was wrong, and it doesn't seem this specific kind failure only. Any better ideas?
TimSort is a highly optimized mergesort, it is stable and faster than old mergesort.
The merge part Now that Timsort has many sets of numbers or “runs” it will perform a sort of modified merge sort on the list. First off, to maintain stability, Timsort does not exchange 2 numbers of equal value. This not only keeps their original positions in the list but enables the algorithm to be faster.
The Comparator and Comparable interface don't do any sorting, so there is no sorting algorithm there. They just compare two Objects, something you need if you want to sort a list of those objects.
After looking at the code of ComparableTimSort
I am not quite sure. Let's analyze it. Here is the only method that throws it (there is a similar method that does the same only with exchanged roles, so analyzing one of them is enough).
private void mergeLo(int base1, int len1, int base2, int len2) {
assert len1 > 0 && len2 > 0 && base1 + len1 == base2;
// Copy first run into temp array
Object[] a = this.a; // For performance
Object[] tmp = ensureCapacity(len1);
int cursor1 = tmpBase; // Indexes into tmp array
int cursor2 = base2; // Indexes int a
int dest = base1; // Indexes int a
System.arraycopy(a, base1, tmp, cursor1, len1);
// Move first element of second run and deal with degenerate cases
a[dest++] = a[cursor2++];
if (--len2 == 0) {
System.arraycopy(tmp, cursor1, a, dest, len1);
return;
}
if (len1 == 1) {
System.arraycopy(a, cursor2, a, dest, len2);
a[dest + len2] = tmp[cursor1]; // Last elt of run 1 to end of merge
return;
}
int minGallop = this.minGallop; // Use local variable for performance
outer:
while (true) {
int count1 = 0; // Number of times in a row that first run won
int count2 = 0; // Number of times in a row that second run won
/*
* Do the straightforward thing until (if ever) one run starts
* winning consistently.
*/
// ------------------ USUAL MERGE
do {
assert len1 > 1 && len2 > 0;
if (((Comparable) a[cursor2]).compareTo(tmp[cursor1]) < 0) {
a[dest++] = a[cursor2++];
count2++;
count1 = 0;
if (--len2 == 0)
break outer;
} else {
a[dest++] = tmp[cursor1++];
count1++;
count2 = 0;
if (--len1 == 1)
break outer;
}
} while ((count1 | count2) < minGallop);
// ------------------ GALLOP
/*
* One run is winning so consistently that galloping may be a
* huge win. So try that, and continue galloping until (if ever)
* neither run appears to be winning consistently anymore.
*/
do {
assert len1 > 1 && len2 > 0;
count1 = gallopRight((Comparable) a[cursor2], tmp, cursor1, len1, 0);
if (count1 != 0) {
System.arraycopy(tmp, cursor1, a, dest, count1);
dest += count1;
cursor1 += count1;
len1 -= count1;
// -->>>>>>>> HERE IS WHERE GALLOPPING TOO FAR WILL TRIGGER THE EXCEPTION
if (len1 <= 1) // len1 == 1 || len1 == 0
break outer;
}
a[dest++] = a[cursor2++];
if (--len2 == 0)
break outer;
count2 = gallopLeft((Comparable) tmp[cursor1], a, cursor2, len2, 0);
if (count2 != 0) {
System.arraycopy(a, cursor2, a, dest, count2);
dest += count2;
cursor2 += count2;
len2 -= count2;
if (len2 == 0)
break outer;
}
a[dest++] = tmp[cursor1++];
if (--len1 == 1)
break outer;
minGallop--;
} while (count1 >= MIN_GALLOP | count2 >= MIN_GALLOP);
if (minGallop < 0)
minGallop = 0;
minGallop += 2; // Penalize for leaving gallop mode
} // End of "outer" loop
this.minGallop = minGallop < 1 ? 1 : minGallop; // Write back to field
if (len1 == 1) {
assert len2 > 0;
System.arraycopy(a, cursor2, a, dest, len2);
a[dest + len2] = tmp[cursor1]; // Last elt of run 1 to end of merge
} else if (len1 == 0) {
throw new IllegalArgumentException(
"Comparison method violates its general contract!");
} else {
assert len2 == 0;
assert len1 > 1;
System.arraycopy(tmp, cursor1, a, dest, len1);
}
}
The method performs a merging of two sorted runs. It does a usual merge but starts "gallopping" once it encounters that one side starts "winning" (I.e., being always less than the other) all the time. Gallopping tries to make things faster by looking ahead more elements instead of comparing one element at a time. Since the runs should be sorted, looking ahead is fine.
You see that the exception is only throw when len1
is 0
at the end.
The first observation is the following: During the usual merge, the exception can never be thrown since the loop aborts directly once len
this 1
. Thus, the exception can only be thrown as result of a gallop.
This already gives a strong hint that the exception behaviour is unreliable: As long as you have small data sets (so small that a generated run may never gallop, as MIN_GALLOP
is 7
) or the generated runs always coincidentally generate a merge that never gallops, you will never receive the exception. Thus, without further reviewing the gallopRight
method, we can come to the conclusion that you cannot rely on the exception: It may never be thrown no matter how wrong your comparator is.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With