Java集合排序Comparable和Comparator

一、概述

对于Java中的对象,我们只能使用基本运算符==!=来判断一下地址是否相等,不能使用><来比较大小。但是在实际的开发中,我们需要对对象进行排序,也就是比较大小,那么应该如何实现呢?

确定两个对象之间的大小关系及排列顺序称为比较,能实现这个比较功能的类或方法称之为比较器,在java中有两种比较器。

  • 内部比较器:Comparable是排序接口,自然排序。
  • 外部比较器:Comparator是比较器接口,定制排序。

ComparableComparator区别如下:

参数 Comparable Comparator
排序逻辑 必须在待排序对象的类中,故称之为自然排序 排序逻辑在另一个实现
排序方法 compareTo(T o) compare(T o1, T o2)
触发排序 Collections.sort(List) Collections.sort(List, Comparator)
所在包 java.lang.Comparable java.util.Comparator

二、Comparable接口

2.1 概述

Comparable是一个基于排序接口,它是自然排序。该接口中只有一个方法compareTo(T o),用于给一个类的多个实例比较大小,进而完成排序。

也就是说某个类实现了Comparable接口,就意味着该类支持排序。通过实现类重写compareTo(T o)方法,从而规定多个实例的自然顺序,然后使用Arrays.sortCollections.sort方法对数组对象或List对象进行排序。

简单点说就是把比较器写在类的内部,一旦实现了Comparable接口,就说明这个类支持排序。

2.2 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方法内部间接的调用了StringcompareTo(T o)方法,所以我们直接就看到了排序的结果。像String、包装类等都实现了Comparable接口,重写了compareTo方法,都清晰的给出了比较两个对象的方式。但是在重写compareTo(T o)方法时都需要遵循这三个规则:

  1. 如果比较者(即this当前对象)大于被比较者(即compareTo方法里面的形参)(即前者大于后者),则返回正整数。
  2. 如果比较者小于被比较者(即前者小于后者),那么返回负整数。
  3. 如果比较者等于被比较者(即前者等于后者),那么返回零。

自定义的类是无法排序的,但是通过实现Comparable接口之后就可以实现,然后通过Arrays.sortCollections.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接口

3.1 概述

Comparator也是一个排序接口,它和Comparable功能是一样的,但是它是定制排序。怎么来理解定制排序呢?

如果某个类没有实现Comparable接口,那么该类本身是不支持排序的,我们就可以使用Comparator来进行排序,或者我们自定义类实现了Comparable接口后,但是自定义类的代码不能再更改了,这时需要改变compareTo(T o)方法中排序的方式,此时也可以选择定制排序Comparator

3.2 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定制排好序了。

3.3 函数式方法

jdk1.8之后又增加了很多静态和默认的新方法,用于函数式编程。

3.3.1 reversed

reversedJava比较器功能接口的默认方法。reversed返回一个比较器,该比较器强制执行反向排序。

default Comparator<T> reversed()

要使用reversed方法,我们需要实例化我们的比较器并调用该方法。

reversed将返回新的比较器实例,该实例将强加该比较器的反向排序。

3.3.2 reverseOrder/naturalOrder

reverseOrder是一个静态方法,返回比较器,对对象集合进行反向自然排序。

对于自然排序,一个类需要实现Comparable并定义compareTo方法。

一个对象集合根据自然排序中的compareTo进行排序。

public static <T extends Comparable<? super T>> Comparator<T> reverseOrder() {
    return Collections.reverseOrder();
}

它在内部调用Collections.reverseOrder()并返回比较器实例。Comparator.reverseOrder反转了自然排序。

IntegerStringDate这样的Java类实现了Comparable接口,并覆盖了其compareTo方法,它们以词汇表(lexicographic-order)排序。

reverseOrder为反向,naturalOrder为正向。

3.3.3 nullsFirst/nullsLast

nullsFirst是比较器功能接口的静态方法。

Comparator.nullsFirst方法返回一个对null友好的比较器,它认为null小于非null

static <T> Comparator<T> nullsFirst(Comparator<? super T> comparator);

找到由nullsFirst方法返回的比较器工作原理。

  1. 空元素被认为是小于非空元素的。
  2. 当两个元素都是空的时候,那么它们就被认为是相等的。
  3. 当两个元素都是非空的时候,指定的比较器决定了顺序。
  4. 如果指定的比较器是空的,那么返回的比较器认为所有非空的元素是相等的。

nullsLastnullsFirst相反。

3.3.4 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中提取一个排序键,并返回一个比较器,使用指定的比较器对该排序键进行比较。

对于intlongdouble数据类型的排序键,比较器分别有comparingIntcomparingLongcomparingDouble方法。

3.3.5 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);

对于intlongdouble数据类型的排序键,比较器分别有thenComparingIntthenComparingLongthenComparingDouble默认方法。

四、总结

总结一下ComparableComparator的使用和区别:

  • Comparable是排序接口,若一个类实现了Comparable接口,就意味着“该类支持排序”。而Comparator是比较器,我们若需要控制某个类的次序,可以建立一个“该类的比较器”来进行排序。
  • Comparablejava.lang包下的,而Comparatorjava.util包下的。
  • Comparable可以看做是内部比较器,而Comparator是外部比较器。
  • Comparable是自然排序,Comparator是定制排序。
  • 如果某个类没有实现Comparable接口,而该类本身是不支持排序的,那么我们就可以使用Comparator来进行定制排序。
  • 或者我们自定义类实现了Comparable接口后,但是自定义类的代码不能再更改了,这时需要改变compareTo(T o)方法中排序的方式,此时也可以选择定制排序Comparator

这两种方法各有优劣:某个类实现了Comparable接口后,在任何地方都可以比较大小,但是有时候需要修改其比较的方式,则需要修原有的代码。而用Comparator的好处就是不需要修改原有代码,而是另外实现一个比较器,当某个自定义的对象需要作比较的时候,把比较器和对象一起传递过去就可以比大小了,并且在Comparator里面用户可以自己实现复杂的可以通用的逻辑,使其可以匹配一些比较简单的对象,那样就可以节省很多重复劳动了。

posted @ 2022-12-15 18:57  夏尔_717  阅读(380)  评论(0编辑  收藏  举报