不少人在生产环境真实撞见过这句报错,却不一定知道它是从哪冒出来的。真实写一个违反传递性的比较器,把这句报错真实、可控地复现出来,顺便搞清楚它的真实触发条件。
上一篇:三元表达式里的拆箱 NPE:真实验证条件选中的分支才会真实拆箱
Comparator 的契约要求满足传递性:如果 a 和 b "相等"、b 和 c "相等",那么 a 和 c 也必须"相等"。一个基于差值容忍度的比较器很容易在不知不觉间打破这一点。
ComparatorContractDemo.java static final Comparator<Integer> fuzzyComparator = (a, b) -> { int diff = a - b; if (Math.abs(diff) < 2) return 0; // 差值在 2 以内就当成"相等" return diff; };
compare(10, 11) = 0 (真实判定为相等,因为差值 1 < 2) compare(11, 12) = 0 (真实判定为相等,因为差值 1 < 2) compare(10, 12) = -2 (差值 2,真实判定为不相等——传递性在这里真实被破坏了)
这种"模糊相等"的比较器在业务代码里并不少见(比如浮点数按精度容忍度比较、时间戳按秒级粒度比较),看起来很合理,但真实数学性质上并不满足传递性——10 和 11"相等"、11 和 12"相等",但 10 和 12 真实不"相等",这在严格的数学关系里是自相矛盾的。
上面那个 fuzzy 比较器在几百个元素的随机数组上不一定能真实触发 JDK 的检测(不是所有违反契约的比较器都会在合并排序过程中暴露出来)。换一个彻底随机、完全不一致的比较器,更容易复现。
RandomCmp.java(更彻底的坏比较器) Comparator<Integer> randomComparator = (a, b) -> rnd.nextBoolean() ? -1 : 1; // 连 compare(x, x) 都不保证返回 0,是最彻底的契约违反
trial=0 没触发 trial=1 没触发 ... trial=6 真实抛出: Comparison method violates its general contract!在 64 个元素的数组上重复排序,真实在第 6 次尝试时触发了 JDK 自带的检测
同样彻底随机的坏比较器,换成一个小很多的数组,重复跑很多次。
size=20, 跑 2000 次, 真实触发次数 = 0
2000 次真实尝试,一次都没有触发。原因在 TimSort 内部的实现细节:数组长度小于 MIN_MERGE(JDK 里定义为 32)时,排序走的是一条简化路径——直接做二分插入排序(binary insertion sort),不会进入真正的"拆分成多段有序 run、再归并"的 TimSort 主流程,而这句报错真实来自归并过程里的一致性检查。数组长度不到 32,这段检测代码根本不会被执行到,不管比较器有多离谱,也不会真实报错——错误的排序结果可能已经产生了,只是没人告诉你。
每一行都是真实运行这段代码得到的结果。