First of all: this is not a duplicate of the question Partial Ordered Comparator but rather builds on it.
My goal is to sort a list of objects (e.g. [2, "a", 1]) in-place such that after sorting no two integers are out of order.
For this, I used the implementation in this answer with the following partial ordering and got a IllegalArgumentException
:
java.lang.IllegalArgumentException: Comparison method violates its general contract!
at java.util.TimSort.mergeHi(TimSort.java:868)
at java.util.TimSort.mergeAt(TimSort.java:485)
at java.util.TimSort.mergeCollapse(TimSort.java:410)
at java.util.TimSort.sort(TimSort.java:214)
at java.util.TimSort.sort(TimSort.java:173)
at java.util.Arrays.sort(Arrays.java:659)
at java.util.Collections.sort(Collections.java:217)
at MySortUtils.sortPartially(ArimsCollectionUtils.java:150)
This is because the proposed comparator has a flaw. Demonstration:
use a partial ordering R
over all Object
instances for which a.before(b)
iff a
and b
are both integers and a < b
according to the integer's natural ordering:
public boolean before(Object a, Object b) {
// only integers are ordered
if (a instanceof Integer && b instanceof Integer) {
int intA = ((Integer) a).intValue();
int intB = ((Integer) b).intValue();
return intA < intB;
} else {
return false;
}
}
The reason for this is that with the following implementation
Comparator<Object> fullCmp = new Comparator<Object>() {
// Implementation shamelessly plucked from
// https://stackoverflow.com/a/16702332/484293
@Override
public int compare(Object o1, Object o2) {
if(o1.equals(o2)) {
return 0;
}
if(partialComparator.before(o1, o2)) {
return -1;
}
if(partialComparator.before(o2, o1)) {
return +1;
}
return getIndex(o1) - getIndex(o2);
}
private Map<Object ,Integer> indexMap = new HashMap<>();
private int getIndex(Object i) {
Integer result = indexMap.get(i);
if (result == null) {
indexMap.put(i, result = indexMap.size());
}
return result;
}
};
this can yield a cycle in the produced ordering, since
// since 2 and "a" are incomparable,
// 2 gets stored with index 0
// "a" with index 1
assert fullCmp.compare(2, "a") == -1
// since "a" and 1 are incomparable,
// "a" keeps its index 1
// 2 gets index 2
assert fullCmp.compare("a", 1) == -1
// since 1 and 2 are comparable:
assert fullCmp.compare(1, 2) == -1
are all true, i.e. 2 < "a", "a" < 1 and "1 < 2, which obviously is not a valid total ordering.
Which leaves me with the final question: How do I fix this bug?
I cannot suggest a full solution for any partial ordering. However for your particular task (comparing integers ignoring anything else) you just have to decide whether integers go before or after anything else. This comparator which assumes that integers go first should work perfectly (using Java-8 syntax):
Comparator<Object> comparator = (a, b) -> {
if(a instanceof Integer) {
if(b instanceof Integer) {
return ((Integer) a).compareTo((Integer) b);
}
return -1;
}
if(b instanceof Integer)
return 1;
return 0;
};
Example:
List<Object> list = Arrays.asList("a", "bb", 1, 3, "c", 0, "ad", -5, "e", 2);
list.sort(comparator);
System.out.println(list); // [-5, 0, 1, 2, 3, a, bb, c, ad, e]