工作中有很多场景需要对元素进行排序,但很多情况下Java提供的默认排序并不能满足场景,比如:

  • 使用Arrays.sortint[]数组进行降序排序,而Arrays.sort()默认提供的是升序排序,需要自定义一个比较器Comparator作为入参传入Arrays.sort方法中;
  • 同样使用Arrays.sort对数组进行排序,只是排序的数组不是int[],而是自定义类的数组排序,或是使用TreeMap存储自定义类的对象,此时自定义类需要实现Comparable接口。

Java提供了两种比较机制:Comparable接口和Comparator接口,下面介绍一下两者的使用方法,并对何时使用哪种比较机制做个小结。

1、Comparable接口

1.1 使用方法

  1. public interface Comparable<T> {
  2. public int compareTo(T o);
  3. }

Comparable接口是个函数式接口,仅需实现一个compareTo方法,当我们需要对自定义的类进行定制化排序时,可以让自定义类实现Comparable接口,至于自定义排序的规则在compareTo方法中实现,对compareTo方法进行说明:

  • compareTo方法返回负整数时,代表this < T o
  • compareTo方法返回正整数时,代表this > T o
  • compareTo方法返回0时,代表this ==T o

其中T ocompareTo方法的入参,this代表当前自定义类的对象。
实际上自定义类实现了Comparable接口中的compareTo方法后,无论是用Arrays.sort()方法排序,还是存储在TreeMap排序,本质上还是按照从“小”到“大”的顺序排列元素的,只不过元素怎么就叫“大”了,这么就叫“小”了,这个比较大小的规则是我们在**compareTo**方法里定义的。

举个例子,Student类有个int类型的属性age,我们想按照age这个维度从大到小对Student对象排序,那么代码应该写成:

  1. public class Student implements Comparable{
  2. private int age;
  3. @Override
  4. public int compareTo(Object o) {
  5. Student student = (Student) o;
  6. return student.age - this.age;
  7. }
  8. }

假设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类型的nameint类型的age,我是按照name的字典序升序排序还是age降序排序,其实就是name这个维度优先级高还是age这个维度优先级高的问题,假设age这个维度的优先级高,我先按age来比较并排序Student对象,当两个Student对象的age不相等时直接比较age就完事了,当age相等时再通过比较name属性判断,如果name属性也相等那这两个Student对象是真的相等了。

1.2 举例说明

下面举个例子,BeanStudent类,Student实例的排序规则是:先按照Student对象的name属性的字典序升序排序,如果name相等则按照age属性降序排序。

  1. package Basic.Compare;
  2. import lombok.Data;
  3. @Data
  4. public class Student implements Comparable{
  5. private String name;
  6. private int age;
  7. public Student(String name, int age)
  8. {
  9. this.name = name;
  10. this.age = age;
  11. }
  12. @Override
  13. public int compareTo(Object o) {
  14. Student student = (Student) o;
  15. if (this.name.compareTo(student.name) != 0)
  16. {
  17. return this.name.compareTo(student.name);
  18. }
  19. else
  20. {
  21. return student.age - this.age;
  22. }
  23. }
  24. }
  1. package Basic.Compare;
  2. import java.util.Set;
  3. import java.util.TreeSet;
  4. public class ComparableMain {
  5. public static void main(String[] args) {
  6. Student student1 = new Student("Cissie", 27);
  7. Student student2 = new Student("Cissie", 28);
  8. Set<Student> set = new TreeSet<>();
  9. set.add(student1);
  10. set.add(student2);
  11. System.out.println(set);
  12. }
  13. }

结果如下:

  1. [Student(name=Cissie, age=28), Student(name=Cissie, age=27)]

需要注意:

  • JavaString类实现了Comparable接口并覆写了compareTo方法,compareTo方法定义的排序规则是按照字典序(默认实现是升序)分别比较两个字符串里的字符(按字符在字符串中的顺序),且这个方法是public的,方便我们在外部直接调用,感兴趣的可以看一下源码;
  • TreeSetTreeMap相比HashSetHashMap多了个排序的特性,因此TreeSetTreeMap里存储的的元素必须实现了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 举例说明

为了避免相同名称的类,这里新建一个beanTeacher类,属性和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接口的写法,写法更简单,理解更直观。
假设现在有个StudentPOJO类,如下:

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