maven-artifact的ComparableVersion与Timsort冲突问题及解决方案咨询
使用maven-artifact的ComparableVersion排序版本时触发异常的问题
问题背景
在项目中使用maven-artifact提供的ComparableVersion对制品版本进行排序,待排序版本以//分隔。
待排序版本列表
0.0.1576817712//0.0.3//0.0.4//0.0.4.//0.0.4.1//0.0.4.2//0.0.4.3//0.0.4.4//0.0.4.5//0.0.5//0.0.5.1//0.0.5.2//0.0.5.3//0.0.5.4//0.0.6//0.0.7//0.0.8//0.1.0//0.1.2//0.1.2.1//0.1.2.2//0.1.2.3//0.1.2.4//0.1.2.5//0.1.2.6//0.1.2.7//0.1.2.8//0.1.3//0.1.3.1//0.1.3.2//0.1.3.3//0.1.4//0.1.4.1//0.1.4.2//0.1.4.3//0.1.4.4//0.1.4.5//0.1.4.6//0.1.4.7//0.1.4.8//0.1.4.9//0.1.5//0.1.7//0.1.7.1//0.1.7.2//0.1.7.3//0.1.7.4//0.1.7.5//0.1.7.6//0.1.7.7//0.1.8//0.1.9//0.1.9.1//0.1.9.11//0.1.9.12//0.1.9.13//0.1.9.2//0.1.9.3//0.1.9.8//0.1.9.9//0.1.9.9.1//1.0.0.1//1.0.0.2//1.0.0.3//1.0.0.3.1//1.0.0.4//1.0.0.5//0.0.0.1-SNAPSHOT//0.0.11-SNAPSHOT//0.0.1576066498-SNAPSHOT//0.0.1576066912-SNAPSHOT//0.0.1576209616-SNAPSHOT//0.0.1576677646-SNAPSHOT//0.0.1576722159-SNAPSHOT//0.0.1576732580-SNAPSHOT//0.0.1576737990-SNAPSHOT//0.0.1576757185-SNAPSHOT//0.0.1576812388-SNAPSHOT//0.0.1576817712-SNAPSHOT//0.0.1576821661-SNAPSHOT//0.0.1576821977-SNAPSHOT//0.0.1576825998-SNAPSHOT//0.0.1577182101-SNAPSHOT//0.0.1577266235-SNAPSHOT//0.0.1577267400-SNAPSHOT//0.0.1577268933-SNAPSHOT//0.0.2-SNAPSHOT//0.0.3-SNAPSHOT//0.0.4-SNAPSHOT//0.0.4.1-SNAPSHOT//0.0.4.2-SNAPSHOT//0.0.4.4-SNAPSHOT//0.0.4.5-SNAPSHOT//0.0.5-SNAPSHOT//0.0.5.11151113-SNAPSHOT//0.0.5.2-SNAPSHOT//0.0.5.2.1-SNAPSHOT//0.0.5.20180829-SNAPSHOT//0.0.5.20181115-SNAPSHOT//0.0.5.4-SNAPSHOT//0.0.5.4.181113-SNAPSHOT//0.0.5.5-SNAPSHOT//0.0.6-SNAPSHOT//0.0.7-SNAPSHOT//0.0.8-SNAPSHOT//0.0.9-SNAPSHOT//0.0.91576063257-SNAPSHOT//0.0.91576066026-SNAPSHOT//0.1.0-SNAPSHOT//0.1.1-SNAPSHOT//0.1.2-SNAPSHOT//0.1.2.2-SNAPSHOT//0.1.2.3-SNAPSHOT//0.1.2.4-SNAPSHOT//0.1.2.5-SNAPSHOT//0.1.2.6-SNAPSHOT//0.1.2.7-SNAPSHOT//0.1.3-SNAPSHOT//0.1.3.1-SNAPSHOT//0.1.3.3-SNAPSHOT//0.1.3.4-SNAPSHOT//0.1.4-SNAPSHOT//0.1.4.1-SNAPSHOT//0.1.4.4-SNAPSHOT//0.1.4.5-SNAPSHOT//0.1.4.6-SNAPSHOT//0.1.4.7-SNAPSHOT//0.1.4.9-SNAPSHOT//0.1.4.l-SNAPSHOT//0.1.4.y-SNAPSHOT//0.1.4.yl-SNAPSHOT//0.1.4.yy-SNAPSHOT//0.1.5-push-SNAPSHOT//0.1.5-SNAPSHOT//0.1.5.1-SNAPSHOT//0.1.5.2-SNAPSHOT//0.1.5.3-SNAPSHOT//0.1.5.4-SNAPSHOT//0.1.5.5-SNAPSHOT//0.1.5.6-SNAPSHOT//0.1.5.l-SNAPSHOT//0.1.5.testpush-SNAPSHOT//0.1.5.y-SNAPSHOT//0.1.5.yl-SNAPSHOT//0.1.6-SNAPSHOT//0.1.7-SNAPSHOT//0.1.7.1-SNAPSHOT//0.1.7.2-SNAPSHOT//0.1.7.3-SNAPSHOT//0.1.7.4-SNAPSHOT//0.1.7.6.1-SNAPSHOT//0.1.8-SNAPSHOT//0.1.9.15-SNAPSHOT//0.1.9.16-SNAPSHOT//0.1.9.2-SNAPSHOT//0.1.9.6-SNAPSHOT//0.1.9.7-SNAPSHOT//0.1.9.9.2-SNAPSHOT//0.1.9.9.3-SNAPSHOT//0.2.0-SNAPSHOT//1.0.0.0-SNAPSHOT//1.0.0.1-SNAPSHOT//1.0.0.2-SNAPSHOT//1.0.0.3-lv-SNAPSHOT//1.0.0.3-SNAPSHOT//1.0.0.4-SNAPSHOT//1.0.0.5-SNAPSHOT//1.0.0.6-SNAPSHOT//1.0.0.7-SNAPSHOT//1.0.0.8-SNAPSHOT//1.0.0.9-SNAPSHOT//1.0.1-SNAPSHOT//1.0.1.0-SNAPSHOT//1.0.1.1-SNAPSHOT//1.0.1.2-SNAPSHOT//1.0.1.3-SNAPSHOT//
代码实现
String data = "...."; // 上述版本字符串 String[] versions = data.split("//"); List<ComparableVersion> comparableVersions = Arrays.stream(versions).map(ComparableVersion::new).collect(Collectors.toList()); Collections.sort(comparableVersions);
触发的异常
java.lang.IllegalArgumentException: Comparison method violates its general contract! at java.util.ComparableTimSort.mergeHi(ComparableTimSort.java:866) at java.util.ComparableTimSort.mergeAt(ComparableTimSort.java:483) at java.util.ComparableTimSort.mergeForceCollapse(ComparableTimSort.java:422) at java.util.ComparableTimSort.sort(ComparableTimSort.java:222) at java.util.Arrays.sort(Arrays.java:1312) at java.util.Arrays.sort(Arrays.java:1506) at java.util.ArrayList.sort(ArrayList.java:1464) at java.util.Collections.sort(Collections.java:143)
已尝试的操作及疑问
- 更换maven-artifact版本(3.8.6、3.8.7、3.6.3),问题依旧。
- 通过JVM参数
-Djava.util.Arrays.useLegacyMergeSort=true启用LegacyMergeSort可解决,但不想影响项目中所有排序逻辑。
疑问:
- ComparableVersion与Timsort冲突的原因是什么?
- 还有哪些针对性的排序解决方案?
问题解答
一、冲突原因
Timsort(Java 7及以上默认排序算法)严格遵守比较器的三大契约:
- 自反性:
a.compareTo(a)必须返回0; - 对称性:若
a.compareTo(b) > 0,则b.compareTo(a) < 0; - 传递性:若
a.compareTo(b) > 0且b.compareTo(c) > 0,则a.compareTo(c) > 0。
而你的版本列表里的特殊格式版本,导致ComparableVersion的比较逻辑违反了这些契约:
- 末尾带点的版本:比如
0.0.4.,解析后本该和0.0.4等价,但部分场景下比较结果不一致,破坏对称性; - 含非数字后缀的版本:比如
0.1.4.l-SNAPSHOT、0.1.5.testpush-SNAPSHOT,这类版本的字符串比较逻辑存在漏洞,导致传递性失效; - 不规范快照版本:部分快照版本的格式不符合Maven规范,使得
ComparableVersion无法稳定判断优先级,进而触发契约违规。
LegacyMergeSort(旧版归并排序)对比较器契约的检查不严格,即使违反规则也能完成排序,所以启用后问题消失。
二、针对性解决方案
1. 预处理版本号(最优解)
先清理不规范的版本格式,从源头避免契约问题:
- 去除版本末尾的点:比如把
0.0.4.替换成0.0.4; - 标准化非数字后缀:要么把
0.1.4.l-SNAPSHOT这类格式统一调整为合法格式,要么根据业务规则标记为无效版本并排除; - 确保快照版本符合Maven规范(比如
x.y.z-SNAPSHOT,不要加多余后缀)。
预处理代码示例:
String cleanedVersion = version.replaceAll("\\.$", "") // 移除末尾的点 .replaceAll("[^0-9a-zA-Z.-]", ""); // 过滤非法字符(可根据业务调整)
2. 自定义比较器修复契约问题
用自定义比较器包裹ComparableVersion,处理特殊场景,确保比较结果符合契约:
Collections.sort(comparableVersions, (v1, v2) -> { // 先执行原有比较逻辑 int result = v1.compareTo(v2); // 处理末尾带点的版本,强制判定为相等 String v1Clean = v1.toString().replaceAll("\\.$", ""); String v2Clean = v2.toString().replaceAll("\\.$", ""); if (v1Clean.equals(v2Clean)) { return 0; } // 处理非数字后缀版本:将非规范版本排在规范版本之后(可根据业务调整规则) boolean v1HasInvalidSuffix = v1.toString().matches(".*[^0-9.-].*"); boolean v2HasInvalidSuffix = v2.toString().matches(".*[^0-9.-].*"); if (v1HasInvalidSuffix && !v2HasInvalidSuffix) { return 1; } else if (!v1HasInvalidSuffix && v2HasInvalidSuffix) { return -1; } return result; });
3. 单独使用LegacyMergeSort
不修改全局JVM参数,手动封装旧版归并排序工具类,仅用于版本排序:
public class LegacyVersionSortUtil { public static <T extends Comparable<? super T>> void sort(List<T> list) { Object[] arr = list.toArray(); legacyMergeSort(arr); ListIterator<T> iterator = list.listIterator(); for (Object obj : arr) { iterator.next(); iterator.set((T) obj); } } private static void legacyMergeSort(Object[] arr) { Object[] aux = arr.clone(); mergeSort(aux, arr, 0, arr.length, 0); } private static void mergeSort(Object[] src, Object[] dest, int low, int high, int off) { int length = high - low; if (length < 7) { for (int i = low; i < high; i++) { for (int j = i; j > low && ((Comparable) dest[j-1]).compareTo(dest[j]) > 0; j--) { swap(dest, j, j-1); } } return; } int mid = (low + high) >>> 1; mergeSort(dest, src, low, mid, off); mergeSort(dest, src, mid, high, off); if (((Comparable) src[mid-1]).compareTo(src[mid]) <= 0) { System.arraycopy(src, low, dest, low, length); return; } for (int i = low, p = low, q = mid; i < high; i++) { if (q >= high || (p < mid && ((Comparable) src[p]).compareTo(src[q]) <= 0)) { dest[i] = src[p++]; } else { dest[i] = src[q++]; } } } private static void swap(Object[] arr, int a, int b) { Object temp = arr[a]; arr[a] = arr[b]; arr[b] = temp; } }
使用时直接调用:
LegacyVersionSortUtil.sort(comparableVersions);
4. 替换版本比较库
改用其他成熟的版本号解析库,比如semver4j或gradle-version,这些库的比较逻辑更严谨,能处理更多不规范格式:
// 示例:使用semver4j List<Semver> semverList = Arrays.stream(versions) .map(Semver::new) .collect(Collectors.toList()); Collections.sort(semverList);
内容的提问来源于stack exchange,提问作者HHHHNNNNNNIIIII
相关产品推荐
相关产品推荐

