工作中有很多场景需要对元素进行排序,但很多情况下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;@Overridepublic 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;@Datapublic class Student implements Comparable{private String name;private int age;public Student(String name, int age){this.name = name;this.age = age;}@Overridepublic 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
