Comparison method violates its general contract!


    今天一個群里哥們兒碰到一個異常,拋到群里求解答,他的代碼如下圖:

拋出的異常信息為:

Java代碼 復制代碼  收藏代碼
  1. java.lang.IllegalArgumentException: Comparison method violates its general contract!  
  2. at java.util.TimSort.mergeHi(TimSort.java:868)  
  3. at java.util.TimSort.mergeAt(TimSort.java:485)  
  4. at java.util.TimSort.mergeCollapse(TimSort.java:408)  
  5. at java.util.TimSort.sort(TimSort.java:214)  
  6. at java.util.TimSort.sort(TimSort.java:173)  
  7. at java.util.Arrays.sort(Arrays.java:659)  
  8. 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不知道,它只要求你的邏輯必須嚴謹,嚴格考慮各種情況下兩者大小的判定原則。所以正確寫法應該是:

Java代碼 復制代碼  收藏代碼
  1. if(o1 == null && o2 == null) {  
  2.     return 0;  
  3. }  
  4. if(o1 == null) {  
  5.     return -1;  
  6. }  
  7. if(o2 == null) {  
  8.     return 1;  
  9. }  
  10. if(o1.getCreateTime() > o2.getCreateTime()) {  
  11.     return 1;  
  12. }  
  13. if(o2.getCreateTime() > o1.getCreateTime()) {  
  14.     return -1;  
  15. }  
  16. 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日志輸出,輸出了文章標題的異常,這個異常是在類似如下代碼中拋出的:

  1. Collections.sort(list, new Comparator<Integer>() {  
  2.     @Override  
  3.     public int compare(Integer o1, Integer o2) {  
  4.         return o1 > o2 ? 1 : -1;// 錯誤的方式  
  5.     }  
  6. });  
Collections.sort(list, new Comparator<Integer>() {
	@Override
	public int compare(Integer o1, Integer o2) {
		return o1 > o2 ? 1 : -1;// 錯誤的方式
	}
});

解決方案

 

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

修改代碼

上面代碼寫的本身就有問題,第4行沒有考慮o1 == o2的情況,再者說我們不需要自己去比較,修改為如下代碼即可:

  1. Collections.sort(list, new Comparator<Integer>() {  
  2.     @Override  
  3.     public int compare(Integer o1, Integer o2) {  
  4.         // return o1 > o2 ? 1 : -1;  
  5.         return o1.compareTo(o2);// 正確的方式  
  6.     }  
  7. });  
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的啟動參數中加入如下參數:

  1. -Djava.util.Arrays.useLegacyMergeSort=true  
-Djava.util.Arrays.useLegacyMergeSort=true

這樣就會照舊使用JDK6的排序算法,在不能修改代碼的情況下,解決這個兼容的問題。

 

分析

在我以前的認知中,高版本的JDK是可以兼容之前的代碼的,與同事討論了一番另加搜索了一番,事實證明,JDK6到JDK7確實存在兼容問題(不兼容列表)。在不兼容列表中我們可以找到關於Collections.sort的不兼容說明,如下:

  1. Area: API: Utilities  
  2. Synopsis: Updated sort behavior for Arrays and Collections may throw an IllegalArgumentException  
  3. Description: The sorting algorithm used by java.util.Arrays.sort and (indirectly) by java.util.Collections.sort has been replaced.   
  4. The new sort implementation may throw an IllegalArgumentException if it detects a Comparable that violates the Comparable contract.   
  5. The previous implementation silently ignored such a situation.  
  6. If the previous behavior is desired, you can use the new system property, java.util.Arrays.useLegacyMergeSort,   
  7. to restore previous mergesort behavior.  
  8. Nature of Incompatibility: behavioral  
  9. 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
再回過頭來看我們開篇有問題的實現:
  1. 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中拋出了本文標題的異常。

 

結論

 

那么現在是否可以蓋棺定論了,按照上面的分析來看,使用這種比較方式(return x > y ? 1 : -1;),只要集合或數組中有相同的元素,就會拋出本文標題的異常。實則不然,什么情況下拋出異常,還取決於JDK7底層排序算法的實現,也就是大名鼎鼎的 TimSort。后面文章會分析TimSort。本文給出一個會引發該異常的Case,以便有心人共同研究,如下:
  1. Integer[] array =   
  2. {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,   
  3. 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};
      (完)


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM