1. 程式人生 > >java.lang.IllegalArgumentException: Comparison method violates its general contract!

java.lang.IllegalArgumentException: Comparison method violates its general contract!

背景
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;// 錯誤的方式 } });

解決方案
先說如何解決,解決方式有兩種。

修改程式碼
上面程式碼寫的本身就有問題,第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);// 正確的方式 } });

不修改程式碼
那麼問題來了。為什麼上面程式碼在JDK6中執行無問題,而在JDK7中卻會拋異常呢?這是因為JDK7底層的排序演算法換了,如果要繼續使用JDK6的排序演算法,可以在JVM的啟動引數中加入如下引數:

-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  

描述的意思是說,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;  

當x == y時,sgn(compare(x, y)) = -1,-sgn(compare(y, x)) = 1,這違背了sgn(compare(x, y)) == -sgn(compare(y, x))約束,所以在JDK7中丟擲了本文標題的異常。
結論
那麼現在是否可以蓋棺定論了,按照上面的分析來看,使用這種比較方式(return x > y ? 1 : -1;),只要集合或陣列中有相同的元素,就會丟擲本文標題的異常。實則不然,什麼情況下丟擲異常,還取決於JDK7底層排序演算法的實現,也就是大名鼎鼎的TimSort。