最近在做產品需求的時候上線了一個新的產品需求,給用戶多了一種新的排序排序規則,更加方便用戶找到自己想要的東西。新版本發布后,QA 給我發了一個 線上崩潰 bug 鏈接,具體內容如下:

看到上面的鏈接,我有點懵逼了,就這排序還能給我搞出 bug 來?看到拋出的異常信息,也沒有見過,於是直接百度搜索了。
一百度,發現很多人遇到這個問題,下面簡單說下出現這個問題的原因:
在 JDK7 版本以上,Comparator 要滿足自反性,傳遞性,對稱性,不然 Arrays.sort,Collections.sort
會報 IllegalArgumentException 異常。
-
自反性:當 兩個相同的元素相比時,compare必須返回0,也就是compare(o1, o1) = 0;
-
反對稱性:如果compare(o1,o2) = 1,則compare(o2, o1)必須返回符號相反的值也就是 -1;
-
傳遞性:如果 a>b, b>c, 則 a必然大於c。也就是compare(a,b)>0, compare(b,c)>0, 則compare(a,c)>0
相信很多人看到這里還是會很懵逼的,感覺自己寫的代碼是不會出現這個問題的,這里理解的主要難點是怎么復現這個崩潰。
任何問題在我們一開始看到的時候,都會覺得很奇怪,覺得自己寫的代碼是不會出現這種問題的,可是一旦復現后,就會突然頓悟了,還是有自己遺漏沒有想到的 case 。
例子
demo1
其實違反上述規則最簡單的例子就是如下:
new Comparator<Student>() { @Override public int compare(Student o1, Student o2) { return o1.getId() > o2.getId() ? 1 : -1; } }
出現原因:沒有考慮相等的情形,所以會拋出異常。
不過對於有基礎的程序猿,一般都會考慮到等號的情形,所以上述代碼還是很少會出現的。
new Comparator<Student>() { @Override public int compare(Student o1, Student o2) { if (o1.getId() == o2.getId()) return 0; return o1.getId() > o2.getId() ? 1 : -1; } }
如果按照上面的來,基本就不會有問題了。當然有個點需要注意的是需要判空。
不過我的崩潰和上面的例子還是很不一樣的,下面舉一個特殊的例子。
demo 2 (線上崩潰例子)
相信大家都用過手機的通訊錄,我手機通訊錄的排序方式是 # AB...YZ 這種形式的。也就是按照用戶名來進行排序的,非字母類型的需要排在前面。
我出代碼的問題其實就出現在對於 # 這一類名字的處理。下面看錯誤代碼:
private static Comparator<CompareObject> mComparatorByPlayingAndLetter = new Comparator<CompareObject>() { @Override public int compare(CompareObject o1, CompareObject o2) { char firstChar = o1.name.charAt(0); char secondChar = o2.name.charAt(0); if (!isUpperLetters(firstChar) && (isUpperLetters(secondChar))) { return 1; } if (isUpperLetters(firstChar) && !isUpperLetters(secondChar)) { return -1; } if (!isUpperLetters(firstChar) && (!isUpperLetters(secondChar))) { return 1; } return o1.name.compareToIgnoreCase(o2.name); } };
這里我先說下自己排序的算法思想:
-
如果一個是大寫字母,一個是非大寫字母,那么很好排序;
-
如果兩個都是非大寫字母,我返回1或者-1都可以,這里我直接給了1,對於非大寫字母后面和大寫字母的比較,前面的邏輯會進行處理,剩下的就是大寫字母之間的比較了。
本地測試,沒問題的。QA 測試也是沒問題。然后這段代碼上線了。
結果昨天剛發布正式版,今天就收到 QA 拋過來的線上崩潰,不過還好只是一個崩潰量。但是為啥會崩潰,我還是沒法理解,我本地測試了很多遍,也還是無法復現。也百度看了很多文章,雖然知道崩潰的理論原因,但是如果無法復現,我就還是不能理解。
並且雖然我有崩潰用戶的 cuid,但是崩潰的用戶的數據排序我是沒法拿到的,也就是還是無法復現。后來自己在已有的數據中,加了一些特殊的字符后,終於復現了。
下面來看一下 ASCII 字符表:

可以看到的是 AB...YZ 是處於后半部分的,數字和大部分特殊符號都是在大寫字母前面,然后有部分標點符號是在大寫字母后面的。
於是,我利用原有的數據,然后再在其中加入大寫字母前后的特殊字符。對於這些數據,除了我這次新增的排序,還有其他排序,比如字母排序,創建時間排序等,不斷對這些數據采用其他排序進行展示,然后再切到出問題的排序,多次來回切換排序算法,最終復現了該問題。

但是具體是哪些數據排序后引起的不滿足規則,由於數據量比較大,我無法確定出來。但是可以知道的是,最后引起崩潰的兩個名字只是雪花,真正有問題的地方在出現問題前就已經埋下了。
那對於上面的問題,如何解決呢?
private static Comparator<CompareObject> mComparatorByPlayingAndLetter = new Comparator<CompareObject>() { @Override public int compare(CompareObject o1, CompareObject o2) { char firstChar = o1.name.charAt(0); char secondChar = o2.name.charAt(0); if (!isUpperLetters(firstChar) && (isUpperLetters(secondChar))) { return 1; } if (isUpperLetters(firstChar) && !isUpperLetters(secondChar)) { return -1; } if (!isUpperLetters(firstChar) && (!isUpperLetters(secondChar))) { return 1; } // 刪除紅色代碼即可 return o1.name.compareToIgnoreCase(o2.name); } };
總之,以后再寫排序比較的時候,對於無法確定大小的情況,交給系統的排序,不要自己去隨意改變比較值,這樣就不會出現這種 case 了。
