JAVA默认排序算法问题
1. 为什么写这篇⽂章
我看见哥哥这篇⽂章的根源是在产品中发现了⼀个诡异的bug:只能在产品环境下重现,在我的本地开发环境⽆法重现,⽽双⽅的代码没有任何区别。最后⽤remote debug的⽅法找到异常所在:
Google了这个错误,是由于Java 7内置的新排序算法导致的。这才猛然想起产品的编译环境最近升级到了Java 7。
2. 结论
在Java 6中Arrays.sort()和Collections.sort()使⽤的是MergeSort,⽽在Java 7中,内部实现换成了,其对对象间⽐较的实现要求更加严格:
Comparator的实现必须保证以下⼏点(出⾃):
⽽我们的代码中,某个compare()实现⽚段是这样的:
这就违背了a)原则:假设X的value为1,Y的value也为1;那么compare(X, Y) ≠ –compare(Y, X)
PS: TimSort不仅内置在各种JDK 7的版本,也存在于Android SDK中(尽管其并没有使⽤JDK 7)。
3. 解决⽅案
花蛋糕
3.1) 更改内部实现:例如对于上个例⼦,就需要更改为满分作文初中
3.2) Java 7预留了⼀个接⼝以便于⽤户继续使⽤Java 6的排序算法:在启动参数中(例如eclip.ini)添加-
Djava.util.Arrays.uLegacyMergeSort=true
3.3) 将这个IllegalArgumentException⼿动捕获住(不推荐)
美国三里岛
4. TimSort在Java 7中的实现
那么为什么Java 7会将TimSort作为排序的默认实现,甚⾄在某种程度上牺牲它的兼容性(在stackoverflow上有⼤量的问题是关于这个新异常的)呢?接下来我们不妨来看⼀看它的实现。
⾸先建议⼤家先读⼀下⽂章以简要理解TimSort的思想。
4.1) 如果传⼊的Comparator为空,则使⽤ComparableTimSort的sort实现。
4.2) 传⼊的待排序数组若⼩于MIN_MERGE(Java实现中为32,Python实现中为64),则
4.3) 开始真正的TimSort过程:
4.3.1) 选取minRun⼤⼩,之后待排序数组将被分成以minRun⼤⼩为区块的⼀块块⼦数组
惊喜若狂4.3.2) 类似于4.2.a找到初始的⼀组升序数列
4.3.3) 若这组区块⼤⼩⼩于minRun,则将后续的数补⾜(采⽤binary sort插⼊这个数组)
志愿服务理念4.3.4) 为后续merge各区块作准备:记录当前已排序的各区块的⼤⼩
4.3.5) 对当前的各区块进⾏merge,merge会满⾜以下原则(假设X,Y,Z为相邻的三个区块):
4.3.6) 重复4.3.2 ~ 4.3.5,直到将待排序数组排序完
4.3.7) Final Merge:如果此时还有区块未merge,则合并它们
5. Demo
这⼀节⽤⼀个具体的例⼦来演⽰整个算法的演进过程:
杨彩妮
*注意*:为了演⽰⽅便,我将TimSort中的minRun直接设置为2,否则我不能⽤很⼩的数组演⽰。。。同时把MIN_MERGE也改成2(默认为32),这样避免直接进⼊binary sort。
6. 如何重现⽂章开始提到的Exception
这⼀节将剥离复杂的业务逻辑,⽤⼀个最简单的例⼦(不修改TimSort.java内置的各种参数)重现⽂章开始提到的Exception。因为尽管google出来的结果中⾮常多的⼈提到了这个Exception及解决⽅案,但并没有⼈给出⼀个可以重现的例⼦和测试数据。另⼀⽅⾯,我也想从其他⾓度来加深对这个问题的理解。
构造测试数据的过程是个反⼈类的过程:( ⼤家不要学我。。
镜子摆放以下是能重现这个问题的代码: