本篇文章讨论ArrayList.sort方法报错:
java.lang.IllegalArgumentException: Comparison method violates its general contract!
的触发条件。
在 JDK7 版本以上,Comparator 要满足自反性,传递性,对称性:
1) 自反性:x,y 的比较结果和 y,x 的比较结果相反。
2) 传递性:x>y,y>z,则 x>z。
3) 对称性:x=y,则 x,z 比较结果和 y,z 比较结果相同。
Comparator不满足自反性并非触发报错的充分条件
又由于测试环境数据的局限性导致测试无法发现非法的Comparator(从经验上看,很多情况是测试环境不报错,而生产环境报错,使得该报错具有隐匿性)
所以了解
java.lang.IllegalArgumentException: Comparison method violates its
general contract!
异常的触发条件具有排查隐患意义
大家可以复制下面代码,自己run一下
import java.util.*;
public class TestList {
public static void main(String[] args) {
//随机产生32个[0~3]的数字集合
Random random = new Random();
ArrayList<Integer> list = new ArrayList<>();
//这里为什么是32?
for (int i = 0; i < 32; i++) {
list.add(random.nextInt(4));
}
System.out.println(list);
list.sort((o1, o2) -> {
//定义一个不具备自反性的Comparator
if(o1>o2){
return 1;
}
return -1;
});
System.out.println(list);
}
}
多次运行会发现两种情况:
[3, 3, 3, 1, 1, 2, 0, 2, 3, 2, 1, 2, 2, 3, 2, 2, 2, 2, 0, 3, 2, 0, 3, 0, 3, 3, 2, 3, 0, 0, 0, 0]
[0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3]
[4, 3, 2, 4, 0, 3, 3, 2, 4, 1, 1, 0, 4, 1, 3, 1, 0, 1, 2, 3, 2, 1, 4, 1, 1, 3, 2, 3, 2, 0, 3, 0]
Exception in thread "main" java.lang.IllegalArgumentException: Comparison method violates its general contract!
at java.util.TimSort.mergeHi(TimSort.java:899)
at java.util.TimSort.mergeAt(TimSort.java:516)
at java.util.TimSort.mergeCollapse(TimSort.java:441)
at java.util.TimSort.sort(TimSort.java:245)
at java.util.Arrays.sort(Arrays.java:1512)
at java.util.ArrayList.sort(ArrayList.java:1464)
at TestList.main(TestList.java:15)
为什么会出现这样两种情况,下面尝试进行分析
首先看一下报错的位置,明确目标:
private void mergeLo(int base1, int len1, int base2, int len2) {
....
} else if (len1 == 0) {
throw new IllegalArgumentException(
"Comparison method violates its general contract!");
} else {
assert len2 == 0;
assert len1 > 1;
System.arraycopy(tmp, cursor1, a, dest, len1);
}
}
private void mergeHi(int base1, int len1, int base2, int len2) {
....
} else if (len2 == 0) {
throw new IllegalArgumentException(
"Comparison method violates its general contract!");
} else {
assert len1 == 0;
assert len2 > 0;
System.arraycopy(tmp, tmpBase, a, dest - (len2 - 1), len2);
}
}
明确目标后从头开始,进入ArrayList.sort(Comparator super E> c)方法
@Override
@SuppressWarnings("unchecked")
public void sort(Comparator<? super E> c) {
//fail-fast机制相关,确保排序过程中没有元素改变
final int expectedModCount = modCount;
//排序方法
Arrays.sort((E[]) elementData, 0, size, c);
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
modCount++;
}
实际调用的是Arrays.sort方法,继续进入:
public static <T> void sort(T[] a, int fromIndex, int toIndex,
Comparator<? super T> c) {
//如果没定义Comparator就使用T的自然排序
if (c == null) {
sort(a, fromIndex, toIndex);
} else {
rangeCheck(a.length, fromIndex, toIndex);
//使用jdk6的排序方法
if (LegacyMergeSort.userRequested)
legacyMergeSort(a, fromIndex, toIndex, c);
else
//主角在这里,大名鼎鼎的TimSort
TimSort.sort(a, fromIndex, toIndex, c, null, 0, 0);
}
}
其中JVM的启动参数中加入如下参数会使用jdk6的排序方法:
-Djava.util.Arrays.useLegacyMergeSort=true
这里按下不表,进入今天的主角TimSort.sort
static <T> void sort(T[] a, int lo, int hi, Comparator<? super T> c,
T[] work, int workBase, int workLen) {
assert c != null && a != null && lo >= 0 && lo <= hi && hi <= a.length;
int nRemaining = hi - lo;
if (nRemaining < 2)
return; // 数组长度是 0 或 1 不需要排序
// 如果数组小于32, 进行不合并的 "mini-TimSort"
if (nRemaining < MIN_MERGE) {
//跳过从lo开始已经有序的元素,,如果运行是递减的,则反转运行(确保方法返回时运行始终是递增的)。
int initRunLen = countRunAndMakeAscending(a, lo, hi, c);
//二分插入排序
binarySort(a, lo, hi, lo + initRunLen, c);
return;
}
/**
*从左到右遍历数组一次,查找自然顺序,将短自然顺序扩展到minRun元素,并合并运行以保持堆栈不变
*/
//创建TimSort实例以维护正在进行的排序的状态。
TimSort<T> ts = new TimSort<>(a, c, work, workBase, workLen);
//返回指定长度数组的最小可接受run长度。短于这个长度的run将通过binarySort进行扩展。
//粗略地说,计算如下:
//MIN_MERGE=32
//If n < MIN_MERGE, return n (小到不需要花里胡哨).
//Else if n 是2的幂, return 16(MIN_MERGE/2).
//Else 返回一个int k,MIN_MERGE/2 <= k <= MIN_MERGE,使得n/k接近,但严格小于2的幂
int minRun = minRunLength(nRemaining);
do {
// 确定下一个run
int runLen = countRunAndMakeAscending(a, lo, hi, c);
// 如果run很小,小于minRun, 就取min(minRun, nRemaining)
if (runLen < minRun) {
int force = nRemaining <= minRun ? nRemaining : minRun;
//二分插入排序
binarySort(a, lo, lo + force, lo + runLen, c);
runLen = force;
}
// 设置run的信息放到记录下来
ts.pushRun(lo, runLen);
//合并run
ts.mergeCollapse();
// 推进下一个run
lo += runLen;
nRemaining -= runLen;
} while (nRemaining != 0);
// 合并所剩下的run
assert lo == hi;
ts.mergeForceCollapse();
assert ts.stackSize == 1;
}
整体的流程为:
由于“mini-TimSort”不需要合并run,因此没有调用到mergeLo或mergeHi,自然也就不会报错。这也是复现测试代码中集合长度为32的原因,也是测试环境很容易忽略非法Comparator的原因。
集合长度大于32也不一定会触发异常,所以继续进入归并逻辑,245:ts.mergeCollapse();
private void mergeCollapse() {
while (stackSize > 1) {
int n = stackSize - 2;
if (n > 0 && runLen[n-1] <= runLen[n] + runLen[n+1]) {
if (runLen[n - 1] < runLen[n + 1])
n--;
mergeAt(n);
} else if (runLen[n] <= runLen[n + 1]) {
mergeAt(n);
} else {
break; // 符合规则
}
}
}
合并相邻两个run
private void mergeAt(int i) {
assert stackSize >= 2;
assert i >= 0;
assert i == stackSize - 2 || i == stackSize - 3;
int base1 = runBase[i];
int len1 = runLen[i];
int base2 = runBase[i + 1];
int len2 = runLen[i + 1];
assert len1 > 0 && len2 > 0;
assert base1 + len1 == base2;
/*
* 记录合并后槽信息,run长度等
*/
runLen[i] = len1 + len2;
if (i == stackSize - 3) {
runBase[i + 1] = runBase[i + 2];
runLen[i + 1] = runLen[i + 2];
}
stackSize--;
/*
* 先用 gallopRight在 run1 里使用二分查找 查找run2 首元素 的位置k, 那么 run1 中k 前面的元素就是合并后最小的那些元素
*/
int k = gallopRight(a[base2], a, base1, len1, 0, c);
assert k >= 0;
base1 += k;
len1 -= k;
if (len1 == 0)
return;
/*
* 在run2 中查找run1 尾元素 的位置len2 ,那么run2 中 len2 后面的那些元素就是合并后最大的那些元素
*/
len2 = gallopLeft(a[base1 + len1 - 1], a, base2, len2, len2 - 1, c);
assert len2 >= 0;
if (len2 == 0)
return;
// 合并剩余的元素,mergeLo指从低向高合并,mergeHi指从高向低合并,因为合并时需要缓存一个run,所及当len1小于等于len2时调用mergeLo
if (len1 <= len2)
mergeLo(base1, len1, base2, len2);
else
mergeHi(base1, len1, base2, len2);
}
mergeLo和mergeHi合并方向相反,其他逻辑相同。我们看一下mergeHi:
private void mergeHi(int base1, int len1, int base2, int len2) {
assert len1 > 0 && len2 > 0 && base1 + len1 == base2;
// 将第二个run放到缓存
T[] a = this.a; // For performance
T[] tmp = ensureCapacity(len2);
int tmpBase = this.tmpBase;
System.arraycopy(a, base2, tmp, tmpBase, len2);
int cursor1 = base1 + len1 - 1; // run1中最后一个未合并元素在a位置
int cursor2 = tmpBase + len2 - 1; // run2中最后一个未合并元素在tmp位置
int dest = base2 + len2 - 1; // a中已合并的位置,这里是从后往前
// 先把run1中最后一个元素a[cursor1]放到a[dest]中,因为进入mergeHi之前计算过run1最后一个元素在run2的位置
a[dest--] = a[cursor1--];
if (--len1 == 0) {
System.arraycopy(tmp, tmpBase, a, dest - (len2 - 1), len2);
return;
}
//mergeAt中gallopRight查过run2首元素在run1中的位置
//所以len2==1代表只剩run2首元素(也就是这个合并中最小的元素)
if (len2 == 1) {
dest -= len1;
cursor1 -= len1;
//把run1剩下的放到a中
System.arraycopy(a, cursor1 + 1, a, dest + 1, len1);
//run2首元素放到a中,结束
a[dest] = tmp[cursor2];
return;
}
Comparator<? super T> c = this.c; // Use local variable for performance
int minGallop = this.minGallop; // 进入“飞奔模式”阈值,会随着“飞奔”的次数而递减
outer:
while (true) {
int count1 = 0; // run1连续获胜的次数,跟“飞奔模式”有关
int count2 = 0; // run2连续获胜的次数
/*
* 从后往前做直接比较,比较run1和temp(run2),dest是标尺,整体排序位置的标志,直到run1或temp获胜7次
*/
do {
assert len1 > 0 && len2 > 1;
if (c.compare(tmp[cursor2], a[cursor1]) < 0) {
a[dest--] = a[cursor1--];
count1++;
count2 = 0;
if (--len1 == 0)
break outer;
} else {
a[dest--] = tmp[cursor2--];
count2++;
count1 = 0;
if (--len2 == 1)
break outer;
}
} while ((count1 | count2) < minGallop);
/*
* 某个run获胜达到阈值后进入“飞奔”
*/
do {
assert len1 > 0 && len2 > 1;
//计算run2最高的待合并元素在run1里的位置,count1既为run1获胜的个数
count1 = len1 - gallopRight(tmp[cursor2], a, base1, len1, len1 - 1, c);
if (count1 != 0) {
dest -= count1;
cursor1 -= count1;
len1 -= count1;
//将run1中获胜元素放到放到a中
System.arraycopy(a, cursor1 + 1, a, dest + 1, count1);
if (len1 == 0)
break outer;
}
a[dest--] = tmp[cursor2--];
if (--len2 == 1)
break outer;
//计算run1最大待合并元素在run2里的位置,count2既为run2获胜的个数
count2 = len2 - gallopLeft(a[cursor1], tmp, tmpBase, len2, len2 - 1, c);
if (count2 != 0) {
dest -= count2;
cursor2 -= count2;
len2 -= count2;
System.arraycopy(tmp, cursor2 + 1, a, dest + 1, count2);
if (len2 <= 1) // len2 == 1 || len2 == 0
break outer;
}
a[dest--] = a[cursor1--];
if (--len1 == 0)
break outer;
minGallop--;
} while (count1 >= MIN_GALLOP | count2 >= MIN_GALLOP);
if (minGallop < 0)
minGallop = 0;
minGallop += 2; // Penalize for leaving gallop mode
} // End of "outer" loop
this.minGallop = minGallop < 1 ? 1 : minGallop; // Write back to field
if (len2 == 1) {
//正常情况run2最小元素再这里最后合并至a
assert len1 > 0;
dest -= len1;
cursor1 -= len1;
System.arraycopy(a, cursor1 + 1, a, dest + 1, len1);
a[dest] = tmp[cursor2]; // Move first elt of run2 to front of merge
} else if (len2 == 0) {
//非法Comparator导致len2为0,抛出异常
throw new IllegalArgumentException(
"Comparison method violates its general contract!");
} else {
assert len1 == 0;
assert len2 > 0;
System.arraycopy(tmp, tmpBase, a, dest - (len2 - 1), len2);
}
}
不满足可逆性的Comparator在运行872行,如果此时碰巧a[cursor1]为run1最小元素
count2 = len2 - gallopLeft(a[cursor1], tmp, tmpBase, len2, len2 - 1, c);
由于gallopLeft内部使用与之前顺序相反的比较
之前compare(run2,run1)
gallopLeft里相反compare(run1,run2)
会出现a[cursor1]比tmp[0]还要小的情况,于是count2=len2-0,最终会将tmp所有元素放到a中,len2会被减为0,这违背了run2[0]是最小元素的预期,最终报错。