集合排序
概述
对于Java中的对象,我们只能使用基本运算符==、!=来判断一下地址是否相等,不能使用>、<来比较大小。但是在实际的开发中,我们需要对对象进行排序,也就是比较大小,那么应该如何实现呢?
确定两个对象之间的大小关系及排列顺序称为比较,能实现这个比较功能的类或方法称之为比较器,在java中有两种比较器。
- 内部比较器:
Comparable是排序接口,自然排序。 - 外部比较器:
Comparator是比较器接口,定制排序。
Comparable和Comparator区别如下:
| 参数 | Comparable | Comparator |
|---|---|---|
| 排序逻辑 | 必须在待排序对象的类中,故称之为自然排序 | 排序逻辑在另一个实现 |
| 排序方法 | compareTo(T o) | compare(T o1, T o2) |
| 触发排序 | Collections.sort(List) | Collections.sort(List, Comparator) |
| 所在包 | java.lang.Comparable | java.util.Comparator |
Comparable接口
概述
Comparable是一个基于排序接口,它是自然排序。该接口中只有一个方法compareTo(T o),用于给一个类的多个实例比较大小,进而完成排序。
也就是说某个类实现了Comparable接口,就意味着该类支持排序。通过实现类重写compareTo(T o)方法,从而规定多个实例的自然顺序,然后使用Arrays.sort或Collections.sort方法对数组对象或List对象进行排序。
简单点说就是把比较器写在类的内部,一旦实现了
Comparable接口,就说明这个类支持排序。
compareTo方法
先看看Comparable接口,它的定义如下:
public interface Comparable<T> {
int compareTo(T o);
}用String举一个简单的例子:
public class CompareTest {
public static void main(String[] args) {
String[] str = new String[]{"AA", "EE", "DD", "CC", "FF", "BB"};
Arrays.sort(str);
System.out.println(Arrays.toString(str));
}
}运行结果为:
[AA, BB, CC, DD, EE, FF]可以发现,在使用Arrays.sort(str)之后就完成了排序,但是我们并没有调用compareTo(T o)方法。这是因为String源码中实现了Comparable接口并且重写了该接口的方法,在使用Arrays.sort(str)时sort方法内部间接的调用了String的compareTo(T o)方法,所以我们直接就看到了排序的结果。像String、包装类等都实现了Comparable接口,重写了compareTo方法,都清晰的给出了比较两个对象的方式。但是在重写compareTo(T o)方法时都需要遵循这三个规则:
- 如果比较者(即
this当前对象)大于被比较者(即compareTo方法里面的形参)(即前者大于后者),则返回正整数。 - 如果比较者小于被比较者(即前者小于后者),那么返回负整数。
- 如果比较者等于被比较者(即前者等于后者),那么返回零。
自定义的类是无法排序的,但是通过实现Comparable接口之后就可以实现,然后通过Arrays.sort或Collections.sort方法排序。我们来看一个自己定义的类怎么使用Comparable接口进行排序:
@Data
@AllArgsConstructor
public class Person implements Comparable {
private String name;
private int age;
// 按名字排序
@Override
public int compareTo(Object o) {
if (o instanceof Person) {
Person p = (Person) o;
// name是String类型,这里直接调用String的compareTo
if (this.name.compareTo(p.name) > 0) {
return 1;
} else if (this.name.compareTo(p.name) < 0) {
return -1;
} else {
return 0;
}
} else {
throw new RuntimeException("传入数据类型不一致...");
}
}
public static void main(String[] args) {
Person[] p = new Person[5];
p[0] = new Person("Jack", 23);
p[1] = new Person("Marry", 13);
p[2] = new Person("Tom", 18);
p[3] = new Person("John", 33);
p[4] = new Person("Thomas", 41);
System.out.println("排序前------------");
for (Person person : p) {
System.out.println(person.getName() + ": " + person.getAge());
}
System.out.println("排序后------------");
Arrays.sort(p);
for (Person person : p) {
System.out.println(person.getName() + ": " + person.getAge());
}
}
}运行结果为:
排序前------------
Jack:23
Marry:13
Tom:18
John:33
Thomas:41
排序后------------
Jack:23
John:33
Marry:13
Thomas:41
Tom:18在Person类中实现了Comparable接口并且重写compareTo(T o)方法,然后我们按照名字排序,可以发现它默认的排序方式是升序,如果要降序则可以在返回值前面加一个负号。
Comparator接口
概述
Comparator也是一个排序接口,它和Comparable功能是一样的,但是它是定制排序。怎么来理解定制排序呢?
如果某个类没有实现Comparable接口,那么该类本身是不支持排序的,我们就可以使用Comparator来进行排序,或者我们自定义类实现了Comparable接口后,但是自定义类的代码不能再更改了,这时需要改变compareTo(T o)方法中排序的方式,此时也可以选择定制排序Comparator。
compare方法
Comparator接口中有一个compare(T o1, T o2)方法,这个方法和compareTo(T o)类似,这个称作外部比较器,定义排序的规则是一样的:
- o1 > o2 (前者大于后者),返回值为正整数。
- o1 < o2 (前者小于后者),返回值为负整数。
- o1 = o2 (前者等于后者),返回值为零。
同样使用String简单举例:
public class CompareTest {
public static void main(String[] args) {
String[] str = new String[]{"AA", "EE", "DD", "CC", "FF", "BB"};
// 使用匿名内部类直接创建
Arrays.sort(str, new Comparator<String>() {
@Override
public int compare(String o1, String o2) {
if (o1.compareTo(o2) > 0) {
return 1;
} else if (o1.compareTo(o2) < 0) {
return -1;
} else {
return 0;
}
}
});
System.out.println(Arrays.toString(str));
}
}我们知道接口是不能被实例化的,这里是匿名内部类的知识,可以自行去度娘寻找答案。
自定义类使用Comparator进行排序:
@Data
@AllArgsConstructor
public class Person {
private String name;
private int age;
public static void main(String[] args) {
Person[] p = new Person[5];
p[0] = new Person("Jack", 23);
p[1] = new Person("Marry", 13);
p[2] = new Person("Tom", 18);
p[3] = new Person("John", 33);
p[4] = new Person("Thomas", 41);
System.out.println("排序前------------");
for (Person person : p) {
System.out.println(person.getName() + ": " + person.getAge());
}
System.out.println("排序后------------");
Arrays.sort(p, new Comparator<Person>() {
// 按照年龄默认排序,如果年龄相同则按照名字默认排序
@Override
public int compare(Person o1, Person o2) {
if (o1 instanceof Person && o2 instanceof Person) {
if (o1.age > o2.age) {
return 1;
} else if (o1.age < o2.age) {
return -1;
} else {
return o1.name.compareTo(o2.name);
}
} else {
throw new RuntimeException("传入数据类型不一致...");
}
}
});
for (Person person : p) {
System.out.println(person.getName() + ": " + person.getAge());
}
}
}程序运行结果:
排序前------------
Jack: 23
Marry: 13
Tom: 18
John: 33
Thomas: 41
排序后------------
Marry: 13
Tom: 18
Jack: 23
John: 33
Thomas: 41这样就使用Comparator定制排好序了。
函数式方法
在jdk1.8之后又增加了很多静态和默认的新方法,用于函数式编程。
reversed
reversed是Java比较器功能接口的默认方法。reversed返回一个比较器,该比较器强制执行反向排序。
default Comparator<T> reversed()要使用reversed方法,我们需要实例化我们的比较器并调用该方法。
reversed将返回新的比较器实例,该实例将强加该比较器的反向排序。
reverseOrder/naturalOrder
reverseOrder是一个静态方法,返回比较器,对对象集合进行反向自然排序。
对于自然排序,一个类需要实现Comparable并定义compareTo方法。
一个对象集合根据自然排序中的compareTo进行排序。
public static <T extends Comparable<? super T>> Comparator<T> reverseOrder() {
return Collections.reverseOrder();
}它在内部调用Collections.reverseOrder()并返回比较器实例。Comparator.reverseOrder反转了自然排序。
像Integer、String和Date这样的Java类实现了Comparable接口,并覆盖了其compareTo方法,它们以词汇表(lexicographic-order)排序。
reverseOrder为反向,naturalOrder为正向。
nullsFirst/nullsLast
nullsFirst是比较器功能接口的静态方法。
Comparator.nullsFirst方法返回一个对null友好的比较器,它认为null小于非null。
static <T> Comparator<T> nullsFirst(Comparator<? super T> comparator);找到由nullsFirst方法返回的比较器工作原理。
- 空元素被认为是小于非空元素的。
- 当两个元素都是空的时候,那么它们就被认为是相等的。
- 当两个元素都是非空的时候,指定的比较器决定了顺序。
- 如果指定的比较器是空的,那么返回的比较器认为所有非空的元素是相等的。
nullsLast和nullsFirst相反。
comparing
comparing是比较器功能接口的静态方法。
Comparator.comparing接受一个函数,该函数从给定的类型中提取一个可比较的排序键,并返回一个通过该排序键进行比较的比较器。
Comparator.comparing有两种形式。
static <T,U extends Comparable<? super U>> Comparator<T> comparing(Function<? super T,
? extends U> keyExtractor);
static <T,U> Comparator<T> comparing(Function<? super T,? extends U> keyExtractor,
Comparator<? super U> keyComparator);我们需要传递一个函数,它将从一个类型T中提取一个可比较的排序键,并返回一个通过该排序键进行比较的比较器。
我们需要传递一个函数和一个比较器。
该方法将从一个类型T中提取一个排序键,并返回一个比较器,使用指定的比较器对该排序键进行比较。
对于int、long和double数据类型的排序键,比较器分别有comparingInt、comparingLong和comparingDouble方法。
thenComparing
thenComparing是比较器功能接口的默认方法。
Comparator.thenComparing返回一个词表顺序(lexicographic-order)的比较器,该比较器被一个比较器实例调用,使用一组排序键对项目进行排序。
当这个比较器比较两个元素相等时,thenComparing方法决定了顺序。
我们可以多次使用Comparator.thenComparing。
当我们想通过排序键组来确定元素的顺序时,要用到它。
default Comparator<T> thenComparing(Comparator<? super T> other);
default <U extends Comparable<? super U>> Comparator<T> thenComparing(Function<? super T,
? extends U> keyExtractor);对于int、long和double数据类型的排序键,比较器分别有thenComparingInt、thenComparingLong和thenComparingDouble默认方法。
总结
总结一下Comparable和Comparator的使用和区别:
Comparable是排序接口,若一个类实现了Comparable接口,就意味着“该类支持排序”。而Comparator是比较器,我们若需要控制某个类的次序,可以建立一个“该类的比较器”来进行排序。Comparable是java.lang包下的,而Comparator是java.util包下的。Comparable可以看做是内部比较器,而Comparator是外部比较器。Comparable是自然排序,Comparator是定制排序。- 如果某个类没有实现
Comparable接口,而该类本身是不支持排序的,那么我们就可以使用Comparator来进行定制排序。 - 或者我们自定义类实现了
Comparable接口后,但是自定义类的代码不能再更改了,这时需要改变compareTo(T o)方法中排序的方式,此时也可以选择定制排序Comparator。
这两种方法各有优劣:某个类实现了Comparable接口后,在任何地方都可以比较大小,但是有时候需要修改其比较的方式,则需要修原有的代码。而用Comparator的好处就是不需要修改原有代码,而是另外实现一个比较器,当某个自定义的对象需要作比较的时候,把比较器和对象一起传递过去就可以比大小了,并且在Comparator里面用户可以自己实现复杂的可以通用的逻辑,使其可以匹配一些比较简单的对象,那样就可以节省很多重复劳动了。
