Java中List和Map排序的坑

問題起源

今天同事找我說一個問題,看一個報錯,報錯原因是:

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大,所以導致了亂序,所以也是同樣的錯誤,第一次遇到這種數值返回造成的隱晦的錯誤。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 1. Java基礎部分 基礎部分的順序:基本語法,類相關的語法,內部類的語法,繼承相關的語法,異常的語法,線程的語...
    子非魚_t_閱讀 31,765評論 18 399
  • 背景 一年多以前我在知乎上答了有關LeetCode的問題, 分享了一些自己做題目的經驗。 張土汪:刷leetcod...
    土汪閱讀 12,769評論 0 33
  • Spring Cloud為開發人員提供了快速構建分布式系統中一些常見模式的工具(例如配置管理,服務發現,斷路器,智...
    卡卡羅2017閱讀 134,923評論 18 139
  • __block和__weak修飾符的區別其實是挺明顯的:1.__block不管是ARC還是MRC模式下都可以使用,...
    LZM輪回閱讀 3,364評論 0 6
  • 愛情 是說不出口的心語 是尋找著你的影子 跟著你的腳步 聽著你說話 看著你笑 我也傻傻嘻哈 見到你苦惱 我也跟著煩...
    小草_d5ad閱讀 287評論 19 49