今天一個群里哥們兒碰到一個異常,拋到群里求解答,他的代碼如下圖:
拋出的異常信息為:
- 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:408)
- 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)
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:408) 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)
我說是compare方法實現的問題,他死活跟我掰,說我之前代碼還好好的啊。沒辦法,我只好根據異常信息提示去翻JDK源碼,異常里提示at java.util.TimSort.mergeHi(TimSort.java:868)即TimSort類的mergeHi方法拋出的。於是我不斷Google,找到了這篇帖子《why does my compare method throw exception — Comparison method violates its general contract》,根據他們的提示,我大概了解了compare方法需要返回1,-1,0即你的返回值要符合約定。
於是我又按照異常提示看了Collections的sort方法源碼,如圖: 繼續跟蹤Arrays類的sort方法:
看到這里我基本就豁然開朗了,因為拋異常的地方是在TimSort類里,說明實際走的是else分支,所以有了第一種解決方法,添加-Djava.util.Arrays.useLegacyMergeSort=true這個JVM參數,其實要真正解決這個問題,要符合規范的實現compare方法,因為他寫的代碼里沒有考慮對象o1和對象o2為Null的情況,即當o1與o2都為null時兩者大小如何判定呢,當o1為null但o2不為null時兩者大小又如何判定了呢,同理當o2為null但o1不為null時兩者大小又如何判定呢又不得而知,或許你又會說,我這兩個對象不可能為null,但那是你認為,JVM不知道,它只要求你的邏輯必須嚴謹,嚴格考慮各種情況下兩者大小的判定原則。所以正確寫法應該是:
- if(o1 == null && o2 == null) {
- return 0;
- }
- if(o1 == null) {
- return -1;
- }
- if(o2 == null) {
- return 1;
- }
- if(o1.getCreateTime() > o2.getCreateTime()) {
- return 1;
- }
- if(o2.getCreateTime() > o1.getCreateTime()) {
- return -1;
- }
- return 0;
if(o1 == null && o2 == null) { return 0; } if(o1 == null) { return -1; } if(o2 == null) { return 1; } if(o1.getCreateTime() > o2.getCreateTime()) { return 1; } if(o2.getCreateTime() > o1.getCreateTime()) { return -1; } return 0;
異常信息
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:408) 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) ...
原因
JDK7中的Collections.Sort方法實現中,如果兩個值是相等的,那么compare方法需要返回0,否則 可能 會在排序時拋錯,而JDK6是沒有這個限制的。
if (len2 == 0) { throw new IllegalArgumentException("Comparison method violates its general contract!"); }
在 JDK7 版本以上,Comparator 要滿足自反性,傳遞性,對稱性,不然 Arrays.sort,
Collections.sort 會報 IllegalArgumentException 異常。
說明:
1) 自反性:x,y 的比較結果和 y,x 的比較結果相反。
2) 傳遞性:x>y,y>z,則 x>z。
3) 對稱性:x=y,則 x,z 比較結果和 y,z 比較結果相同。
反例:下例中沒有處理相等的情況,實際使用中可能會出現異常:
new Comparator<Student>() { @Override public int compare(Student o1, Student o2) { return o1.getId() > o2.getId() ? 1 : -1; } }
背景
16號為了統一線上服務器運行環境,將兩台服務器的Tomcat6+JDK6升級到Tomcat7+JDK7,本以為很簡單的事情,升級后自己驗證也沒問題,沒想到卻悲劇了。升級后,過了半小時運營就找過來反饋問題,部分角色無法登陸系統,由於異常日志沒有輸出,沒有找到問題,無奈回滾。今天我們就來說說JDK6升級到JDK7會遇到的坑。本文為了方便搜索,就直接以異常信息作為文章標題了。
復現
回滾后,到beta環境按照線上的權限配置,復現該問題,加上了error日志輸出,輸出了文章標題的異常,這個異常是在類似如下代碼中拋出的:
- Collections.sort(list, new Comparator<Integer>() {
- @Override
- public int compare(Integer o1, Integer o2) {
- return o1 > o2 ? 1 : -1;// 錯誤的方式
- }
- });
Collections.sort(list, new Comparator<Integer>() { @Override public int compare(Integer o1, Integer o2) { return o1 > o2 ? 1 : -1;// 錯誤的方式 } });
解決方案
先說如何解決,解決方式有兩種。
修改代碼
上面代碼寫的本身就有問題,第4行沒有考慮o1 == o2的情況,再者說我們不需要自己去比較,修改為如下代碼即可:
- Collections.sort(list, new Comparator<Integer>() {
- @Override
- public int compare(Integer o1, Integer o2) {
- // return o1 > o2 ? 1 : -1;
- return o1.compareTo(o2);// 正確的方式
- }
- });
Collections.sort(list, new Comparator<Integer>() { @Override public int compare(Integer o1, Integer o2) { // return o1 > o2 ? 1 : -1; return o1.compareTo(o2);// 正確的方式 } });
不修改代碼
那么問題來了。為什么上面代碼在JDK6中運行無問題,而在JDK7中卻會拋異常呢?這是因為JDK7底層的排序算法換了,如果要繼續使用JDK6的排序算法,可以在JVM的啟動參數中加入如下參數:
- -Djava.util.Arrays.useLegacyMergeSort=true
-Djava.util.Arrays.useLegacyMergeSort=true
這樣就會照舊使用JDK6的排序算法,在不能修改代碼的情況下,解決這個兼容的問題。
分析
在我以前的認知中,高版本的JDK是可以兼容之前的代碼的,與同事討論了一番另加搜索了一番,事實證明,JDK6到JDK7確實存在兼容問題(不兼容列表)。在不兼容列表中我們可以找到關於Collections.sort的不兼容說明,如下:
- Area: API: Utilities
- Synopsis: Updated sort behavior for Arrays and Collections may throw an IllegalArgumentException
- Description: The sorting algorithm used by java.util.Arrays.sort and (indirectly) by java.util.Collections.sort has been replaced.
- The new sort implementation may throw an IllegalArgumentException if it detects a Comparable that violates the Comparable contract.
- The previous implementation silently ignored such a situation.
- If the previous behavior is desired, you can use the new system property, java.util.Arrays.useLegacyMergeSort,
- to restore previous mergesort behavior.
- Nature of Incompatibility: behavioral
- RFE: 6804124
Area: API: Utilities Synopsis: Updated sort behavior for Arrays and Collections may throw an IllegalArgumentException Description: The sorting algorithm used by java.util.Arrays.sort and (indirectly) by java.util.Collections.sort has been replaced. The new sort implementation may throw an IllegalArgumentException if it detects a Comparable that violates the Comparable contract. The previous implementation silently ignored such a situation. If the previous behavior is desired, you can use the new system property, java.util.Arrays.useLegacyMergeSort, to restore previous mergesort behavior. Nature of Incompatibility: behavioral RFE: 6804124
描述的意思是說,java.util.Arrays.sort(java.util.Collections.sort調用的也是此方法)方法中的排序算法在JDK7中已經被替換了。如果違法了比較的約束新的排序算法也許會拋出llegalArgumentException異常。JDK6中的實現則忽略了這種情況。那么比較的約束是什么呢?看這里,大體如下:
- sgn(compare(x, y)) == -sgn(compare(y, x))
- ((compare(x, y)>0) && (compare(y, z)>0)) implies compare(x, z)>0
- compare(x, y)==0 implies that sgn(compare(x, z))==sgn(compare(y, z)) for all z
- return x > y ? 1 : -1;
return x > y ? 1 : -1;當x == y時,sgn(compare(x, y)) = -1,-sgn(compare(y, x)) = 1,這違背了sgn(compare(x, y)) == -sgn(compare(y, x))約束,所以在JDK7中拋出了本文標題的異常。
結論
- Integer[] array =
- {0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
- 0, 0, 0, 1, 1, 0, 0, 1, 0, 1, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 2, 1, 0, 0, 0, 2, 30, 0, 3};
Integer[] array = {0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 0, 1, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 2, 1, 0, 0, 0, 2, 30, 0, 3};