一文搞懂多字段排序

914 阅读5分钟

本文主要使用Comparable和Comparator接口来实现对象的排序,以及二维数组的排序功能。

概念

首先了解一下自然排序和定制排序

自然排序:Comparable接口强行对实现它的每个类的对象进行整体排序。这种排序被称为类的自然排序,类的 compareTo(T t) 方法被称为它的自然比较方法。当前对象this与指定对象t比较“大小”,如果当前对象this大于指定对象t,则返回正整数,如果当前对象this小于指定对象t,则返回负整数,如果当前对象this等于指定对象t,则返回零。

定制排序:强行对某个对象 collection 进行整体排序 的比较函数。可以将 Comparator 传递给 sort 方法(如 Collections.sort 或 Arrays.sort),从而允许在排序顺序上实现精确控制。还可以使用 Comparator 来控制某些数据结构(如有序 set或有序映射)的顺序,或者为那些没有自然顺序的对象 collection 提供排序。

当元素的类型没有实现java.lang.Comparable接口而又不方便修改代码,或者实现了java.lang.Comparable接口的排序规则不适合当前的操作,那么可以考虑使用 Comparator 的对象来排序。

区别comparable相当于内部比较器,comparator相当于外部比较器

废话不多说直接上干货

1. Comparable

如果一个类实现了Comparable接口,则意味着该类支持排序,则可以通过Collections.sort(list), Arrays.sort(arr)直接排序,同时对于一些本身就带有排序属性的集合来说,也会按照compareTo方法定义好的排序规则在add元素时进行排序,如PriorityQueue,TreeMap,TreeSet等

1. 先看下他的源码
// 源码非常简单,只有一个方法,实现它即可
public interface Comparable<T> {
    /**
     * Compares this object with the specified object for order.  Returns a
     * negative integer, zero, or a positive integer as this object is less
     * than, equal to, or greater than the specified object.
     */
    public int compareTo(T o);
}
2.这里创建一个Student类并实现了Comparable
public class Student implements Comparable<Student> {
   private Integer id;
​
   private Integer age;
​
   private String address;
​
   public Student(Integer id, Integer age, String address) {
      this.id = id;
      this.age = age;
      this.address = address;
   }
​
   public Integer getId() {
      return id;
   }
​
   public void setId(Integer id) {
      this.id = id;
   }
​
   public Integer getAge() {
      return age;
   }
​
   public void setAge(Integer age) {
      this.age = age;
   }
​
   public String getAddress() {
      return address;
   }
​
   public void setAddress(String address) {
      this.address = address;
   }
​
   @Override
   public String toString() {
      return "Student{" +
            "id=" + id +
            ", age=" + age +
            ", address='" + address + ''' +
            '}';
   }
​
   /**
    * 按id升序,age降序,address字典序升序(比较的是ASCII值)
    *
    * @param 指定对象
    * @return
    */
   @Override
   public int compareTo(Student o) {
      return this.id.compareTo(o.getId()) == 0 ? o.getAge().compareTo(this.age) == 0 ?
            this.address.compareTo(o.address) : o.getAge().compareTo(this.age) : this.id.compareTo(o.getId());
   }
3. 然后创建一些测试数据
    List<Student> list = new ArrayList<>();
    list.add(new Student(3, 3, "B"));
    list.add(new Student(3, 2, "A"));
    list.add(new Student(10, 13, "b"));
    list.add(new Student(10, 13, "c"));
    list.add(new Student(10, 13, "a"));
    list.add(new Student(6, 23, "w"));
    list.add(new Student(6, 12, "w"));
    list.add(new Student(6, 12, "a"));
    return list;
4. 编写测试类

注意:需要注意的是加入队列后,由于队列是将数组构建成堆的数据结构,所以断点看的时候好像是没有排序的,但是当你使用poll数据时,他其实有序的

public static void main(String[] args) {
   List<Student> students = SortUtil.creatData();
   SortUtil.myPrint(students);
   System.out.println("--------------------集合工具类------------------");
   // 使用collections
   Collections.sort(students);
   students.forEach(stu -> System.out.println(stu));
   System.out.println("------------------优先队列-------------------");
   // 使用优先队列
   PriorityQueue<Student> queue = new PriorityQueue<>();
   SortUtil.creatData().forEach(stu -> queue.offer(stu));
   while (!queue.isEmpty()) {
      System.out.println(queue.poll());
   }
   System.out.println("----------------TreeMap---------------");
   // 使用TreeMap,TreeSet源码就是使用TreeMap,不重复测试了
   Map<Student, String> map = new TreeMap<>();
   SortUtil.creatData().forEach(stu -> map.put(stu, null));
   map.forEach((k, v) -> System.out.println(k));
}
5. 查看运行结果,可以看到排序功能正确
--------------------------------
Student{id=3, age=3, address='B'}
Student{id=3, age=2, address='A'}
Student{id=10, age=13, address='b'}
Student{id=10, age=13, address='c'}
Student{id=10, age=13, address='a'}
Student{id=6, age=23, address='w'}
Student{id=6, age=12, address='w'}
Student{id=6, age=12, address='a'}
--------------------集合工具类------------------
Student{id=3, age=3, address='B'}
Student{id=3, age=2, address='A'}
Student{id=6, age=23, address='w'}
Student{id=6, age=12, address='a'}
Student{id=6, age=12, address='w'}
Student{id=10, age=13, address='a'}
Student{id=10, age=13, address='b'}
Student{id=10, age=13, address='c'}
------------------优先队列-------------------
Student{id=3, age=3, address='B'}
Student{id=3, age=2, address='A'}
Student{id=6, age=23, address='w'}
Student{id=6, age=12, address='a'}
Student{id=6, age=12, address='w'}
Student{id=10, age=13, address='a'}
Student{id=10, age=13, address='b'}
Student{id=10, age=13, address='c'}
----------------TreeMap---------------
Student{id=3, age=3, address='B'}
Student{id=3, age=2, address='A'}
Student{id=6, age=23, address='w'}
Student{id=6, age=12, address='a'}
Student{id=6, age=12, address='w'}
Student{id=10, age=13, address='a'}
Student{id=10, age=13, address='b'}
Student{id=10, age=13, address='c'}

2. 实现Comparator

Comparator也叫做定制排序,是比较接口,对于类本身不支持排序,或者实现了Comparable接口,但是排序不符合需求时,则可以使用Comparator实现集合外部排序

Comparator体现了一种策略模式,不改变自身,通过替换不同的comparator比较器就能变换不同的比较规则,更加灵活

1.源码
@FunctionalInterface
public interface Comparator<T> {
    /**
     * Compares its two arguments for order.  Returns a negative integer,
     * zero, or a positive integer as the first argument is less than, equal
     * to, or greater than the second.<p>
     **/
    int compare(T o1, T o2);

对于集合工具类,一般都有sort方法,如下所示,只要实现Comparator接口,就能进行排序

public static <T> void sort(List<T> list, Comparator<? super T> c) {
    list.sort(c);
}

对于有序集合,则它们就会有将Comparator作为入参的构造方法,如

// 优先队列
public PriorityQueue(Comparator<? super E> comparator) {
    this(DEFAULT_INITIAL_CAPACITY, comparator);
}
// treemap
public TreeMap(Comparator<? super K> comparator) {
    this.comparator = comparator;
}
2. 如何使用

以Collections类为例,其他同理

// 1.实现自定义类
class MySort implements Comparator<Student> {
   @Override
   public int compare(Student o1, Student o2) {
      return o1.getAge().compareTo(o2.getAge());
   }
}
Collections.sort(students, new MySort());
// 2.匿名内部类
Collections.sort(students, new Comparator<Student>() {
    @Override
    public int compare(Student o1, Student o2) {
        return o1.getAge().compareTo(o2.getAge());
    }
});
// 3. lambda表达式
Collections.sort(students, (s1, s2) -> s1.getId().compareTo(s2.getId()) == 0 ?
                s2.getAge().compareTo(s1.getAge()) == 0 ? s1.getAddress().compareTo(s2.getAddress()) :
                        s2.getAge().compareTo(s1.getAge()) : s1.getId().compareTo(s2.getId()));
// 4. 方法接口
Collections.sort(students, Comparator.comparing(Student::getId).thenComparing(Student::getAge).reversed()
                 .thenComparing(Student::getAddress));

利用stream流的sort方法,需要注意的是,流排序之后返回的新数组才是排好序的结果,原数组保持不变

List<Student> collect = SortUtil.creatData().stream().sorted(new MySort()).collect(Collectors.toList());

其他类型基本大同小于,不再过多演示了

3. 二维数组的自定义排序

最后在介绍下更加灵活的排序,二维数组如何对指定列进行排序 每一列按照不同规则排序sortRule = new int[1,0,1]; 这里1表示升序,-1表示降序


Arrays.sort(elem, new Comparator<int[]>() {
    @Override
    public int compare(int[] o1, int[] o2) {
        int flag;
        // 这里的return并不是有一个不相等就返回,而是每个数据都会遍历到,且返回值只是作为比较大小用的,并不是退出方法
        for (int i = 0; i < o1.length; i++) {
            flag = o1[i] - o2[i];
            if (flag != 0) {
                return flag * sortRule[i];
            }
        }
        return 0;
    }
});

Arrays.sort的底层使用了二分和归并排序,当数组长度小于32,则使用过二分排序,大于等于32,则使用归并阿排序

int[][] arrays = new int[][]{{2, 1, 1}, {1, 2, 2}, {1, 1, 4}, {1, 2, 3}};
// 表示按第一列升序,第二列升序
Arrays.sort(arrays, (arr1, arr2) -> arr1[0] - arr2[0] == 0 ? arr1[1] - arr2[1] : arr1[0] - arr2[0]);