摘要
java的比较器分两种,也即是内外比较器,内部比较器是comparable接口,外部比较器是comparator接口,为什么分内外比较器呢。
内部比较器:comparable,当需要对某个类的对象进行排序的时候,则需要该类实现comparable接口,重写comparaTo方法,实现这个接口的类的对象列表,可使用Array.sort或者Collections.sort进行排序,
外部比较器:comparator接口,匿名内部类的形式存在,重写内部的compare方法,可对对象数组或者集合使用Arrays.sort(数组,比较器)或者Collections.sort(集合,比较器)进行排序
compara和comparaTo方法共同之处,就是如果比较的数比被比较的数小返回-1,
例: a.comparaTo(b) a小,返回-1 ,a大返回1,compara同理
可以查看我的博客
https://itzmn.github.io/2018/11/16/java%E7%9A%84%E6%AF%94%E8%BE%83%E5%99%A8%E5%B0%8F%E7%BB%93/
Comparable
comparaTo方法
类实现comparable接口,都要重写comparaTo方法,举个栗子,
Integer类中的方法
//Integer实现了Comparable接口
public final class Integer extends Number implements Comparable {
//比较两个数值
public int compareTo(Integer anotherInteger) {
return compare(this.value, anotherInteger.value);
}
//比较两个数值,如果前面一个大,返回1,小返回-1,相等返回0
public static int compare(int x, int y) {
return (x < y) ? -1 : ((x == y) ? 0 : 1);
}
String类中
//实现了接口
public final class String
implements java.io.Serializable, Comparable, CharSequence {
//String类中的compareTo方法
public int compareTo(String anotherString) {
//得到调用者的值的长度
int len1 = value.length;
//得到比较的值的长度
int len2 = anotherString.value.length;
//查看最小长度
int lim = Math.min(len1, len2);
char v1[] = value;
char v2[] = anotherString.value;
int k = 0;
//循环到最小长度的值结束
while (k < lim) {
char c1 = v1[k];
char c2 = v2[k];
//比较每一个字符
if (c1 != c2) {
//返回字符的ASCII码差值
return c1 - c2;
}
k++;
}
//如果前面的字符都一样,返回长度差
return len1 - len2;
}
以上是java内置好的一些类,实现了接口,重写了方法,如果我们自己类想要实现接口,可以重写这个方法,自定义排序的方法
例:
Class B implements Comparable{
int comparaTo(B b){
//return的值按照自己是想怎么排序进行返回
return
}
}
当我们的类实现了Comparable接口,那么类对象集合,或者数组可使用工具类进行排序。
可以使用
Arrays.sort(数组,null),Collections.sort(集合,null)进行排序
Comparator
comparator一般是外比较器,以匿名内部类的形式存在,对数据进行比较
可新建一个比较器的对象,实现比较器内部的compara方法
//新建一个比较器
new Comparator(){
@Override
public int compare(Integer str1, Integer str2){
return str1.compareTo(str2);
}
};
可使用Arrays.sort(数组,比较器)或者Collections.sort(集合,比较器)进行排序
Collections.sort()
了解了比较器,我们看一下一些工具类内到底是如何将数组进行排序的
我们举个栗子,跟踪源码看一下
模拟数据
10 14 7 8 12
ArrayList integers = new ArrayList<>();
integers.add(10);
integers.add(14);
integers.add(7);
integers.add(8);
integers.add(12);
integers.add(3);
Collections.sort(integers, new Comparator(){
@Override
public int compare(Integer str1, Integer str2){
return str1.compareTo(str2);
}
});
开始调用
我们点进源码看一下
1.第一步
这是Collections的sort方法,调用了list.sort方法,
@SuppressWarnings({"unchecked", "rawtypes"})
public static void sort(List list, Comparator super T> c) {
list.sort(c);
}
2.第二步
我们点进list.sort()方法,这里他将集合数据转成了数组传入后面。数组默认长度是10,但是只有size个数据。但是list是个接口,所以我们查看其中一个实现类的实现方法,ArrayList的实现方法,
//这是ArrayList的sort方法。
public void sort(Comparator super E> c) {
final int expectedModCount = modCount;
//调用了Arrays.sort 并将比较器传了进来,
Arrays.sort((E[]) elementData, 0, size, c);
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
modCount++;
}
3.第三步
我们点进Arrays.sort方法查看,我们一条一条看
public static void sort(T[] a, int fromIndex, int toIndex,
Comparator super T> c) {
if (c == null) {
sort(a, fromIndex, toIndex);
} else {
rangeCheck(a.length, fromIndex, toIndex);
if (LegacyMergeSort.userRequested)
legacyMergeSort(a, fromIndex, toIndex, c);
else
TimSort.sort(a, fromIndex, toIndex, c, null, 0, 0);
}
}
1.如果比较器为null,这个我们一会再看,(记住这个,我们会回来的)
2.rangeCheck(),看名字就可以看出,这是一个检验下标是否越界的方法,就不细说了,
3.下面这个判断,我也不大懂,但是每次他都是进入了else进行排序
4.第四步
我们进入TimSort.sort方法,传入的数据是 a:数组数据,也就是集合中的数据,formIndex:0,toIndex:是刚刚集合的size。
static 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; // Arrays of size 0 and 1 are always sorted
// If array is small, do a "mini-TimSort" with no merges
if (nRemaining < MIN_MERGE) {
int initRunLen = countRunAndMakeAscending(a, lo, hi, c);
binarySort(a, lo, hi, lo + initRunLen, c);
return;
}
//下面有些代码,都没看了,就看到这
.....
}
我们还是一行一行看
1.断言,判断下标是否正确
2.得到集合的数据长度,并判断是否小于2,也即是判断有没有排序的必要
3.如果大于,判断数据是否小于32,我们看的是小于,大于的代码没看
4.int initRunLen = countRunAndMakeAscending(a, lo, hi, c);
我们看一下这个方法是干什么的
下面点进入,贴代码
4.1 countRunAndMakeAscending代码
//传入的参数,a是数组,lo是开始位置默认是0,hi是结束位置默认是刚刚集合的长度
private static int countRunAndMakeAscending(T[] a, int lo, int hi,
Comparator super T> c) {
assert lo < hi;
int runHi = lo + 1;
if (runHi == hi)
return 1;
// Find end of run, and reverse range if descending
if (c.compare(a[runHi++], a[lo]) < 0) { // Descending
//判断后面一个值是否是小于当前值的。
while (runHi < hi && c.compare(a[runHi], a[runHi - 1]) < 0)
runHi++;
reverseRange(a, lo, runHi);
} else { // Ascending
while (runHi < hi && c.compare(a[runHi], a[runHi - 1]) >= 0)
runHi++;
}
return runHi - lo;
}
解释一下方法的参数
a是数组,lo是开始位置默认是0,hi是结束位置默认是刚刚集合的长度(下面称集合长度),c是比较器
1.断言,开始索引小于传入的集合长度
2.runHi变成了开始坐标加一
3.如果这个数值和集合长度相同,则返回1,
4.判断,并且将runHi加1,在这里我们刚刚在比较器内部的操作效果就显示出来了。在这里,我们将坐标靠后面的数与该坐标数值进行比较,如果这里比较用的就是我们自己写在比较器里面的比较
5.如果返回-1 ,说明后面一个值小,这两个数构成了降序,然后还是接着判断后面一个值和当前的值大小,直到降序被打断,记录这个坐标,一会返回这个坐标。
如果被打断,进入另一个方法,将这一段降序的数组,反转,改成升序,
// 反转这部分的数组
private static void reverseRange(Object[] a, int lo, int hi) {
hi--;
while (lo < hi) {
//交换数组的值
Object t = a[lo];
a[lo++] = a[hi];
a[hi--] = t;
}
}
6.如果返回大于0,说明后面一个值大,这两个值构成了升序。然后继续比较runHi位置和前面一个位置的数值,如果都是后面的大,就一直向后,直到数值变化为小的,也即是一直是升序,突然升序变了,记录下这个位置并返回这个位置。
4.1结束,回到上一个方法
5,第五步
我们返回了一个坐标,现在这个数组的开始到这个坐标以前,已经满足了递增的规律,然后我们需要将整个数据进行排序。进入了
binarySort(a, lo, hi, lo + initRunLen, c);方法
// 传入数组,开始坐标,结束坐标,start是上一步,0加上已经排序好的坐标的后一位
private static void binarySort(T[] a, int lo, int hi, int start,
Comparator super T> c) {
assert lo <= start && start <= hi;
if (start == lo)
start++;
for ( ; start < hi; start++) {
T pivot = a[start];
// Set left (and right) to the index where a[start] (pivot) belongs
int left = lo;
int right = start;
assert left <= right;
while (left < right) {
int mid = (left + right) >>> 1;
if (c.compare(pivot, a[mid]) < 0)
right = mid;
else
left = mid + 1;
}
assert left == right;
int n = start - left; // The number of elements to move
switch (n) {
case 2: a[left + 2] = a[left + 1];
case 1: a[left + 1] = a[left];
break;
default: System.arraycopy(a, left, a, left + 1, n);
}
a[left] = pivot;
}
}
首先我们看一下传入的参数
// 传入数组,lo开始坐标,hi结束坐标,start是上一步,0加上已经排序好的坐标的后一位
也就是说现在数组有hi个数据,但是从lo到start的数据是已经满足递增的数据了,前面已经看过了,现在我们需要将后面的数据和前面的数据汇合,形成整个递增。
1.如果start==lo,说明前面没有递增数据,所以将坐标加1,为后面准备
2.循环,start以后都是尚未排序的,我们需要将后面的数据插入到前面的有序序列,
3.保存当前位置的数值,
4.得到已经递增的数据的左坐标,也就是lo,得到递增数据的右坐标也即是start
5.然后将当前位置的值与递增数据的中间位置的数值进行比较,如果小于,那么说明当前值是插在递增数据的前半段,将递增数据的右坐标改成中间位置的坐标,如果是大于,那么是后半段,将递增数据的左坐标改成中间位置,直到找到正确位置,判断当前值的位置与正确位置的差,如果小与2则进行移动,如果大于则进行拷贝
6.循环将以上数值全部弄完,这样我们就可以得到整个有序数列了。
我们现在回到第三步的第一小步,如果比较器为null,
进入sort(a, fromIndex, toIndex);
3.1
//对没有比较器的数组进行比较
public static void sort(Object[] a, int fromIndex, int toIndex) {
rangeCheck(a.length, fromIndex, toIndex);
if (LegacyMergeSort.userRequested)
legacyMergeSort(a, fromIndex, toIndex);
else
ComparableTimSort.sort(a, fromIndex, toIndex, null, 0, 0);
}
3.2
//进入了ComparableTimSort的sort方法 ,传入数组,起始坐标与结束坐标。
static void sort(Object[] a, int lo, int hi, Object[] work, int workBase, int workLen) {
assert a != null && lo >= 0 && lo <= hi && hi <= a.length;
int nRemaining = hi - lo;
if (nRemaining < 2)
return; // Arrays of size 0 and 1 are always sorted
// If array is small, do a "mini-TimSort" with no merges
if (nRemaining < MIN_MERGE) {
int initRunLen = countRunAndMakeAscending(a, lo, hi);
binarySort(a, lo, hi, lo + initRunLen);
return;
}
这里面的几步如
int initRunLen = countRunAndMakeAscending(a, lo, hi);
binarySort(a, lo, hi, lo + initRunLen);
都与上面的几步差不多,不过内部的比较方法不一样
private static int countRunAndMakeAscending(Object[] a, int lo, int hi) {
assert lo < hi;
int runHi = lo + 1;
if (runHi == hi)
return 1;
// Find end of run, and reverse range if descending
if (((Comparable) a[runHi++]).compareTo(a[lo]) < 0) { // Descending
while (runHi < hi && ((Comparable) a[runHi]).compareTo(a[runHi - 1]) < 0)
runHi++;
reverseRange(a, lo, runHi);
} else { // Ascending
while (runHi < hi && ((Comparable) a[runHi]).compareTo(a[runHi - 1]) >= 0)
runHi++;
}
return runHi - lo;
}
因为比较器为null,所以会将值转成比较器进行比较,存储的值,都是实现了Comparable接口的类,内部的比较方法也都重写了,会按照自带的比较。
在最后的比较阶段也是一样的。
private static void binarySort(Object[] a, int lo, int hi, int start) {
assert lo <= start && start <= hi;
if (start == lo)
start++;
for ( ; start < hi; start++) {
Comparable pivot = (Comparable) a[start];
// Set left (and right) to the index where a[start] (pivot) belongs
int left = lo;
int right = start;
assert left <= right;
while (left < right) {
int mid = (left + right) >>> 1;
if (pivot.compareTo(a[mid]) < 0)
right = mid;
else
left = mid + 1;
}
assert left == right;
int n = start - left; // The number of elements to move
// Switch is just an optimization for arraycopy in default case
switch (n) {
case 2: a[left + 2] = a[left + 1];
case 1: a[left + 1] = a[left];
break;
default: System.arraycopy(a, left, a, left + 1, n);
}
a[left] = pivot;
}
}
Arrays.sort
Arrays.sort在上面已经被调用过了,可以看一下
这样我们就把比较器的代码大致看完了 ,看这个源码,我是传数据debug看的,这样数据改变更方面看一些,这里面我比较喜欢,是在数据排序的时候,这个是直接插入法,但是它优化了一点是,插入不是一个一个比较,而是和有序序列的中间值比较,这样更快。看完之后我对比较器的理解更深了一层。