工作中有很多场景需要对元素进行排序,但很多情况下Java提供的默认排序并不能满足场景,比如:
- 使用
Arrays.sort
对int[]
数组进行降序排序,而Arrays.sort()默认提供的是升序排序,需要自定义一个比较器Comparator
作为入参传入Arrays.sort
方法中; - 同样使用
Arrays.sort
对数组进行排序,只是排序的数组不是int[]
,而是自定义类的数组排序,或是使用TreeMap存储自定义类的对象,此时自定义类需要实现Comparable
接口。
Java提供了两种比较机制:Comparable
接口和Comparator
接口,下面介绍一下两者的使用方法,并对何时使用哪种比较机制做个小结。
1、Comparable接口
1.1 使用方法
public interface Comparable<T> {
public int compareTo(T o);
}
Comparable
接口是个函数式接口,仅需实现一个compareTo
方法,当我们需要对自定义的类进行定制化排序时,可以让自定义类实现Comparable
接口,至于自定义排序的规则在compareTo
方法中实现,对compareTo
方法进行说明:
- 当
compareTo
方法返回负整数时,代表this <T o
; - 当
compareTo
方法返回正整数时,代表this >T o
; - 当
compareTo
方法返回0时,代表this ==T o
;
其中T o
是compareTo
方法的入参,this
代表当前自定义类的对象。
实际上自定义类实现了Comparable
接口中的compareTo
方法后,无论是用Arrays.sort()
方法排序,还是存储在TreeMap
排序,本质上还是按照从“小”到“大”的顺序排列元素的,只不过元素怎么就叫“大”了,这么就叫“小”了,这个比较大小的规则是我们在**compareTo**
方法里定义的。
举个例子,Student
类有个int
类型的属性age
,我们想按照age
这个维度从大到小对Student
对象排序,那么代码应该写成:
public class Student implements Comparable{
private int age;
@Override
public int compareTo(Object o) {
Student student = (Student) o;
return student.age - this.age;
}
}
假设this.age
= 27,student.age
= 28,此时student.age
- this.age
= 1 > 0,即compareTo
方法返回正整数,根据这个写好的比较Student
对象大小的规则我们判定this
> T o
,因为本质上还是会按照自定义类对象从“小”到“大”的顺序排列元素,因此排序时会将T o
放前面,this
放后面,结果就是student.age
= 28排在了this.age
= 27的前面,实现了降序排序。
排序是按照不同的维度进行比较排序的,不同维度之间有优先级顺序,比如Student类有两个属性,String
类型的name
和int
类型的age
,我是按照name
的字典序升序排序还是age
降序排序,其实就是name
这个维度优先级高还是age
这个维度优先级高的问题,假设age这个维度的优先级高,我先按age
来比较并排序Student
对象,当两个Student
对象的age
不相等时直接比较age
就完事了,当age
相等时再通过比较name
属性判断,如果name
属性也相等那这两个Student
对象是真的相等了。
1.2 举例说明
下面举个例子,Bean
是Student
类,Student
实例的排序规则是:先按照Student
对象的name
属性的字典序升序排序,如果name
相等则按照age
属性降序排序。
package Basic.Compare;
import lombok.Data;
@Data
public class Student implements Comparable{
private String name;
private int age;
public Student(String name, int age)
{
this.name = name;
this.age = age;
}
@Override
public int compareTo(Object o) {
Student student = (Student) o;
if (this.name.compareTo(student.name) != 0)
{
return this.name.compareTo(student.name);
}
else
{
return student.age - this.age;
}
}
}
package Basic.Compare;
import java.util.Set;
import java.util.TreeSet;
public class ComparableMain {
public static void main(String[] args) {
Student student1 = new Student("Cissie", 27);
Student student2 = new Student("Cissie", 28);
Set<Student> set = new TreeSet<>();
set.add(student1);
set.add(student2);
System.out.println(set);
}
}
结果如下:
[Student(name=Cissie, age=28), Student(name=Cissie, age=27)]
需要注意:
Java
的String
类实现了Comparable
接口并覆写了compareTo
方法,compareTo
方法定义的排序规则是按照字典序(默认实现是升序)分别比较两个字符串里的字符(按字符在字符串中的顺序),且这个方法是public
的,方便我们在外部直接调用,感兴趣的可以看一下源码;TreeSet
和TreeMap
相比HashSet
和HashMap
多了个排序的特性,因此TreeSet
和TreeMap
里存储的的元素必须实现了Comparable
接口。2、Comparator接口
2.1 使用方法
Comparator
接口也是函数式接口,里面有很多被default
关键字修饰的方法,我们不需要关注这些被default
关键字修饰的方法,只需要关心必须覆写的compare
方法:
@FunctionalInterface
public interface Comparator<T> {
int compare(T o1, T o2);
}
- 当
compare
方法的返回值为负整数时,代表o1
<o2
; - 当
compare
方法的返回值为正整数时,代表o1
>o2
; - 当
compare
方法的返回值为0时,代表o1
==o2
;
与Comparable
接口一样,**Comparator**
接口本质上还是会按照从“小”到“大”的顺序对元素进行排序,至于元素比较大小的规则就是在**compare**
方法中通过返回值确定了。使用时需要new
一个实现了Comparator
接口的比较器comparator
,将comparator
作为参数传入排序方法(比如Arrays.sort())中。
2.2 举例说明
为了避免相同名称的类,这里新建一个bean
:Teacher
类,属性和Student
类相同,new
一个Comparator
接口的比较器comparator
,对Teacher
类的元素进行排序,排序规则是:先按照name属性的字典序降序排序,如果name属性值相等,则按照age属性降序排序。
import lombok.Data;
@Data
public class Teacher {
private String name;
private int age;
public Teacher(String name, int age)
{
this.name = name;
this.age = age;
}
}
import java.util.Set;
import java.util.TreeSet;
public class ComparatorMain {
public static void main(String[] args) {
Teacher teacher1 = new Teacher("Jerry", 27);
Teacher teacher2 = new Teacher("Cissie", 28);
Set<Teacher> set = new TreeSet<>((o1, o2) -> o1.getName().equals(o2.getName()) ? o2.getAge() - o1.getAge() : o2.getName().compareTo(o1.getName()));
set.add(teacher1);
set.add(teacher2);
System.out.println(set);
}
}
需要注意:
TreeSet
有一种构造方式就是构造函数的传入参数为一个Comparator
接口的实现类;- 使用Lambda表达式可以让函数式编程的写法更加简洁(B格更高)。
2.3 同时存在Comparable接口和Comparator接口
当排序时同时存在Comparable
接口和Comparator
接口该按照哪个来呢?答案是按照**Comparator**
接口来,即排序时同时存在**Comparable**
接口和**Comparator**
接口,**Comparator**
接口的优先级更高。
举个例子,对于Student
类(代码与2.1中的一样)的实例排序,Comparable
接口的排序规则是先按照Student
对象的name
属性的字典序升序排序,如果name
相等则按照age
属性降序排序。排序比较器Comparator
接口的排序规则是先按照Student
对象的name
属性的字典序升序排序,如果name
相等则按照age
属性升序排序,代码如下:
public class BothExistMain {
public static void main(String[] args) {
Student student1 = new Student("Cissie", 27);
Student student2 = new Student("Cissie", 28);
Student[] array = new Student[]{student1, student2};
Arrays.sort(array, (o1, o2) -> o1.getName().equals(o2.getName()) ? o1.getAge() - o2.getAge() : o1.getName().compareTo(o2.getName()));
System.out.println(Arrays.toString(array));
}
}
执行结果如下:
[Student(name=Cissie, age=27), Student(name=Cissie, age=28)]
可见结果是按照排序比较器Comparator
接口里制定的排序规则进行排序的。
2.4 增强版的Comparator接口: Comparator.comparing
很多排序方法都需要传入一个比较器来定义排序规则,之前我们可能通过实现Comparator
接口,以lambda
表达式的形式嵌入到sort
方法里,这里介绍一种增强版的Comparator
接口的写法,写法更简单,理解更直观。
假设现在有个Student
的POJO
类,如下:
package com.Jerry.comparator;
import lombok.Data;
@Data
public class Student {
private String name;
private int age;
private int id;
}
有下面4个常用的排序方法:
// 单条件升序
list.sort(Comparator.comparing(Student::getId);
// 单条件降序
list.sort(Comparator.comparing(Student::getId).reversed());
// 多条件升序
list.sort(Comparator.comparing(Student::getId).thenComparing(Student::getName));
// 多条件升序降序
list.sort(Comparator.comparing(Student::getId).thenComparing(Student::getName, Comparator.reverseOrder()));
用Comparator.comparing
的方便之处在于:
- 可以直观地指定
POJO
类的某个属性来定义排序规则,默认升序; - 加一个
reversed()
就可以实现降序; - 后面跟多个
thenComparing()
就可以实现多条件排序,不必用lambda表达式写多个三目表达式。
举例:以上面Student
类为例,先按学生姓名升序排序,再按age降序排序,最后按id升序排序,如下:
package com.Jerry.comparator;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
public class StudentSortDemo {
public static void main(String[] args) {
List<Student> studentList = new ArrayList<>();
studentList.add(new Student("Jerry", 27, 1));
studentList.add(new Student("Jerry", 27, 2));
studentList.add(new Student("Jerry", 28, 3));
studentList.add(new Student("Cissie", 27, 4));
// 先按学生姓名升序排序,再按age降序排序,最后按id升序排序
studentList.sort(Comparator.comparing(Student::getName).thenComparing(Student::getAge,
Comparator.reverseOrder()).thenComparing(Student::getId));
studentList.forEach(System.out::println);
}
}
结果:
Student(name=Cissie, age=27, id=4)
Student(name=Jerry, age=28, id=3)
Student(name=Jerry, age=27, id=1)
Student(name=Jerry, age=27, id=2)
注意:
这里按age降序排序为什么不是直接用reversed()而是thenComparaing里又传入了一个参数:Comparator.reverseOrder()?
比如用下面的写法:
studentList.sort(Comparator.comparing(Student::getName).thenComparing(Student::getAge).reversed().thenComparing(Student::getId));
如果用上述写法,则排序结果如下:
Student(name=Jerry, age=28, id=3)
Student(name=Jerry, age=27, id=1)
Student(name=Jerry, age=27, id=2)
Student(name=Cissie, age=27, id=4)
并不是我们想要的,没有按照优先按照姓名升序排序。
但如果我们仅对一个thenComparing进行reversed(0,效果还是正确的,比如:
studentList.sort(Comparator.comparing(Student::getName).reversed());
结果:
Student(name=Jerry, age=27, id=1)
Student(name=Jerry, age=27, id=2)
Student(name=Jerry, age=28, id=3)
Student(name=Cissie, age=27, id=4)
所以:
当遇到POJO的多条件升序降序排序时,不要用reversed(),而是在thenComparing里传一个Comparator.reverseOrder()!
参考链接2说的就是这个问题,但是也没说清楚。
3、map中按照key和value进行排序
可以使用Stream的相关API对map中的key和value进行排序,以下获得是排序后的stream流,并非排序后的map:
// key升序
map.entrySet().stream().sorted((Map.Entry.comparingByKey()));
// key降序
map.entrySet().stream().sorted(Map.Entry.<Integer, String>comparingByKey().reversed());
// value升序
map.entrySet().stream().sorted((Map.Entry.comparingByValue()));
// value降序
map.entrySet().stream().sorted((Map.Entry.<Integer, String>comparingByValue().reversed()));
- comparingByKey和comparingByValue适用于对map进行中间操作(排序),最后的结果一般不是map,只是中间会对map的key或者value进行排序。
如果想获取一个排序后的map,可以使用TreeMap,并自定义一个排序器,在初始化这个TreeMap时构造函数里传入自定义的排序器,如下: ```java TreeMap
map = new TreeMap (new Comparator () { @Override public int compare(String o1, String o2) {
return o2.compareTo(o1);
}
});
// 向初始化后的TreeMap插入k-v时,会按照排序器指定的规则排序 map.put(key, value); ```
如果map里存放的元素想按照插入顺序排序,可以使用LinkedHashMap。
4、小结
下面对
Comparable
接口和Comparator
接口进行小结:很多JDK定义好的类,比如String类、Integer类等都实现了
Comparable
接口,且默认是升序排序;- 对于一些自定义的类,可能在不同场景下需要不同的排序规则,如果用
Comparable
接口则只能在类中定义一种排序规则,显得不够灵活,此时可以用Comparator
接口,创建不同规则的比较器(Comparator
接口实现类的实例)来排序; - 当排序时同时存在
Comparable
接口和Comparator
接口,Comparator
接口的优先级高于Comparable
接口; Comparator
接口比Comparable
接口更灵活。- 当对
POJO
类进行排序时用Comparator.comparing
更方便!参考:
JAVA8-用lamda表达式和增强版Comparator进行排序
JAVA8 Stream之Sort排序comparing 和thenComparing