JDK 7 引入 TimSort 替代傳統 MergeSort 作為 `Arrays.sort` / `Collections.sort` 的預設算法,對比較器的一致性要求更嚴格。違反「自反性、對稱性、傳遞性」三大契約的比較器在 JDK 7+ 會拋出 IllegalArgumentException: Comparison method violates its general contract。本文通過真實業務案例剖析根因,提供修復比較器與 JVM 參數繞過兩種解法。
JDK 7 釋出說明:
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.
TimSort(Python 發源,結合歸併與插入排序)在運行過程中會主動檢測比較器是否滿足:
舊版 MergeSort 靜默容忍違規,TimSort 則直接拋異常終止排序。
需求:按價格升序 → 價格相同按起飛時間升序 → 仍相同按屬性優先級:非共享非經停 > 非經停 > 非共享 > 經停。
public int compare(FlightPrice o1, FlightPrice o2) {
// 最高優先級:非共享非經停
if (o1.getStopNumber() == 0 && !o1.isShare()) {
return -1; // o1 排前面
} else if (o2.getStopNumber() == 0 && !o2.isShare()) {
return 1; // o2 排前面
}
// 次優先級:非經停
else if (o1.getStopNumber() == 0) {
return -1;
} else if (o2.getStopNumber() == 0) {
return 1;
}
// 次優先級:非共享
else if (!o1.isShare()) {
return -1;
} else if (!o2.isShare()) {
return 1;
}
// 最低優先級:經停
else if (o1.getStopNumber() > 0) {
return -1;
} else if (o2.getStopNumber() > 0) {
return 1;
}
return 0;
}
當 o1、o2 同時滿足「非共享非經停」時:
compare(o1, o2) → 進入第一個 if 返回 -1(o1 < o2)compare(o2, o1) → 同樣進入第一個 if 返回 -1(o2 < o1)違反對稱性:`compare(o1, o2) == -1` 但 `compare(o2, o1) == -1`,應為 `+1`。TimSort 檢測到此矛盾即拋異常。
Comparator<FlightPrice> comparator = Comparator
.comparingInt(FlightPrice::getPrice) // 價格升序
.thenComparing(FlightPrice::getDepartureTime) // 起飛時間升序
.thenComparing((f1, f2) -> { // 屬性優先級
boolean f1Best = f1.getStopNumber() == 0 && !f1.isShare();
boolean f2Best = f2.getStopNumber() == 0 && !f2.isShare();
if (f1Best != f2Best) return f1Best ? -1 : 1;
boolean f1NoStop = f1.getStopNumber() == 0;
boolean f2NoStop = f2.getStopNumber() == 0;
if (f1NoStop != f2NoStop) return f1NoStop ? -1 : 1;
boolean f1NoShare = !f1.isShare();
boolean f2NoShare = !f2.isShare();
if (f1NoShare != f2NoShare) return f1NoShare ? -1 : 1;
return Integer.compare(f1.getStopNumber(), f2.getStopNumber());
});
List<FlightPrice> sorted = list.stream().sorted(comparator).collect(Collectors.toList());
public int compare(FlightPrice o1, FlightPrice o2) {
// 1. 價格
int priceDiff = Integer.compare(o1.getPrice(), o2.getPrice());
if (priceDiff != 0) return priceDiff;
// 2. 起飛時間
int timeDiff = o1.getDepartureTime().compareTo(o2.getDepartureTime());
if (timeDiff != 0) return timeDiff;
// 3. 屬性優先級:計算權重分數,分數高者排前
int score1 = calcPriorityScore(o1);
int score2 = calcPriorityScore(o2);
return Integer.compare(score2, score1); // 分數高排前 -> 降序
}
private int calcPriorityScore(FlightPrice f) {
int score = 0;
if (f.getStopNumber() == 0 && !f.isShare()) score += 1000; // 非共享非經停
else if (f.getStopNumber() == 0) score += 100; // 非經停
else if (!f.isShare()) score += 10; // 非共享
// 經停 score += 0
return score;
}
若無法立即修復比較器(如第三方庫),可啟用舊版 MergeSort:
-Djava.util.Arrays.useLegacyMergeSort=true
警告:此為臨時方案,JDK 10+ 已移除 Legacy MergeSort,必須修復比較器邏輯。
TimSort 並非在排序前全量檢查比較器,而是在排序過程中發現違規才拋異常。某些資料順序剛好繞過檢測路徑,導致「同樣的比較器、同樣的資料,有時拋異常有時不拋」,極大增加排查難度。
Comparator.comparing(...).thenComparing(...) 鏈式構建,編譯期保證契約。compare 時,嚴格遵循「先比主鍵,再比次鍵,最後兜底返回 0」結構,絕不出現「先到先得」的非對稱邏輯。ComparatorTester 工具)。Comparison method violates its general contract 是 TimSort 對比較器契約的嚴格執法。根因在於業務比較器邏輯不嚴謹(常見於多條件「先到先得」寫法)。徹底修復需重寫比較器,採用權重分數或 Comparator 鏈式 API 保證嚴格弱序關係。JVM 參數繞過僅適用於緊急止血,不可長期依賴。