問題起源
今天同事找我說一個問題,看一個報錯,報錯原因是:
Exception in thread "main" java.lang.IllegalArgumentException: Comparison method violates its general contract!
at java.util.TimSort.mergeLo(TimSort.java:777)
at java.util.TimSort.mergeAt(TimSort.java:514)
at java.util.TimSort.mergeCollapse(TimSort.java:441)
at java.util.TimSort.sort(TimSort.java:245)
at java.util.Arrays.sort(Arrays.java:1512)
at java.util.ArrayList.sort(ArrayList.java:1454)
at java.util.Collections.sort(Collections.java:175)
at com.asiainfo.miaohq.test.Test1.main(Test1.java:18)
很容易寫個小的驗證程序:
public class SortTest {
/**
* @Title: main
* @Description:
* @param args
*/
public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
for (int i = 0; i < 65; i++) {
list.add(i);
list.add(null);
}
Collections.sort(list, new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
int a = 0;
if (o1 == null)
a = 1;
else if (o2 == null)
a = -1;
else
a = o1.compareTo(o2);
return a;
}
});
for (Integer i : list) {
System.out.println(i);
}
}
}
說明:如果是循環次數不是65更少的數字,偶爾報錯,偶爾
不報錯,實際原因和算法有關系,里面進行分組排序然后合并(只是猜測)。
查找原因
去看了下異常棧,里面是個復雜的排序算法,也沒耐心去查下去了,搜了下,大概的意思是排序不符合邏輯規則。
具體例子:
假設a>b b>c 那么a>c一定成立的。
目前上面的例子存在 問題是如果o1為null,則在任何情況下,都是null>o2,
但是其實存在o2==null的情況,這就導致了null> null 的邏輯錯誤。
解決辦法
1 添加屬性設置
System.setProperty("java.util.Arrays.useLegacyMergeSort", "true");
2 判斷null情況:
public class SortTest {
/**
* @Title: main
* @Description:
* @param args
*/
public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
for (int i = 0; i < 65; i++) {
list.add(i);
list.add(null);
}
Collections.sort(list, new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
int a = 0;
if(o1 == null && o2== null)
return 0;
if (o1 == null)
a = 1;
else if (o2 == null)
a = -1;
else
a = o1.compareTo(o2);
return a;
}
});
for (Integer i : list) {
System.out.println(i);
}
}
}
國外例子
下面代碼,你可以看出為什么也會報這個錯誤嗎?
public int compare(Node o1, Node o2)
{
HashMap<Integer,Integer> childMap = orderMap.get(parentID);
if(childMap != null && childMap.containsKey(o1.getID()) &&
childMap.containsKey(o2.getID()))
{
int order1 = childMap.get(o1.getID());
int order2 = childMap.get(o2.getID());
if(order1<order2)
return -1;
else if(order1>order2)
return 1;
else
return 0;
}
else
return 0;
}
外國人解釋:
比較方法是不傳遞的。舉個例子如果 A==B 和B==C,那么
A一定等于C。
現在看這個例子的情況:
假設A、B、C三個對象情況。假設包含情況是這樣的:
childMap.containsKey(A.getID()) returns true
childMap.containsKey(B.getID()) returns false
childMap.containsKey(C.getID()) returns true
當A和B比較的時候,外面的if條件不滿足,所以返回結果是0,意味著A==B
當B和C比較的時候,外面的if條件仍然不滿足,所以結果也是0,意味著B==C。
假設A和C比較的時候很有可能返回1或-1 ,這就造成的結果是A!=C.
這就違反了傳遞規則。
所以不能在else里面直接返回0,需要根據情況判斷。
額外的坑
后面同事又測試發現一個問題,代碼片段如下:
public class FFileTimeCompartor implements Comparator<Map.Entry<String, FTPFile>> {
@Override
public int compare(Entry<String, FTPFile> map1, Entry<String, FTPFile> map2) {
if (map1 == null && map2 == null) {
return 0;
}
if (map1 == null)
return 1;
if (map2 == null)
return -1;
FTPFile file1 = map1.getValue();
FTPFile file2 = map2.getValue();
long res = file1.getTimestamp().getTimeInMillis() - file2.getTimestamp().getTimeInMillis();
return (int) (res == 0 ? file1.getName().compareTo(file2.getName()) : res);
}
}
仍然是報上面的錯誤,后面終于發現是file1.getTimestamp().getTimeInMillis() 比較是long類型,那么相差的差值在轉成int的時候,由于可能被int大,所以導致了亂序,所以也是同樣的錯誤,第一次遇到這種數值返回造成的隱晦的錯誤。