Java 数组排序时 Comparator 的使用

发布时间 2023-12-15 04:36:10作者: MeYokYang

Java 数组排序时 Comparator 的使用

Arrays.sort

Java 对数组排序可使用Arrays.sort方法。该方法有以下版本:

public static void sort(t[] a) {}
public static void sort(t[] a, int fromIndex, int toIndex) {}
public static void sort(Object[] a) {}
public static void sort(Object[] a, int fromIndex, int toIndex) {}
public static <T> void sort(T[] a, Comparator<? super T> c) {}
public static <T> void sort(T[] a, int fromIndex, int toIndex, Comparator<? super T> c)

t[]中的t表示基本数据类型。

对于基本类型无法提供Comparator来定制比较器。不过,基本类型可以装箱,可先将其装箱成对象数组,然后对该数组使用指定的比较器排序,最后再拆箱成基本类型数组。这可以使用流来进行处理,如将int[] arr进行反序:

int[] arr = new int[16];
// fill arr array
arr = Arrays.stream(arr).boxed().sorted(Collections.reverseOrder()).mapToInt(Integer::intValue).toArray();

使用Arrays.sort的泛型版本进行数组排序时,必须提供一个Comparator比较器,数组中的元素是根据该比较器来确定顺序的。

Comparator

Comparator本身是一个函数式接口,但它提供了一些方法来让我们获取Comparator对象(你也可以自己实现Comparator接口并创建对象)。

comparing

静态方法comparing实现根据对象的“键”比较(对象的“键”被定义为什么需要我们自己提供,即“取键函数”)。comparing方法中会以数组元素为参数调用该“取键函数”得到元素的“键”,然后使用“键”的compareTo方法。如:

String[] strings = new String[16];
// fill strings array
Arrays.sort(strings, Comparator.comparing(s -> s));
Arrays.sort(strings, Comparator.comparing(String::length));

由于这里sort方法必须提供一个比较器,当我们想使用对象本身进行比较时,“取键函数”可以被定义为s -> s

第二个sort示例中,我们使用元素的长度进行比较。

comparing源码如下(其中的一个版本):

public static <T, U extends Comparable<? super U>> Comparator<T> comparing(
        Function<? super T, ? extends U> keyExtractor)
{
    Objects.requireNonNull(keyExtractor);
    return (Comparator<T> & Serializable)
        (c1, c2) -> keyExtractor.apply(c1).compareTo(keyExtractor.apply(c2));
}
  • comparing返回一个Comparator对象sort的第二个参数使用。Comparator是一个函数式接口,return语句返回 lambda 表达式简化书写。

  • 由于需要调用“键”的compareTo方法,所以U具有类型限制U extends Comparable<? super U>

    为什么不是限制为U extends Comparable<U>

    因为Comparable的泛型类型为U的父类其实也可以执行。如SubClass继承SuperClassSuperClass实现Comparable<SuperClass>,如果是U extends Comparable<U>,这里USubClass就会失败(SubClass不是Comparable<SubClass>)。但SubClassComparable<SuperClass>,限制为U extends Comparable<? super U>USubClass时,?可以匹配上SuperClass

  • 泛型T为元素的类型,U为“键”的类型。

    为什么keyExtractor类型被限制为Function<? super T, ? extends U>而不是Function<T, U>

    为了更宽泛地接受类型。

    • 对于T,使用通配符?而不是T那么我们可以使用以下语句:

      Arrays.sort(subClasses, Comparator.comparing(SuperClass::length));
      

      这里TSubClassSuperClassSubClass的父类,SubClass包含length方法,但我们不用一定要写成SubClass::length

    • 对于U,使用通配符?可以让“取键函数”的返回值为U的子类。当我们明确要求U的类型时(出于某种目的,如下面reversed介绍到的问题),那么我们就可以不把传入的返回类型限定为U

      Arrays.sort(arr, Comparator.<ElementType, SuperClass>comparing(elemant -> new SubClass()));
      
  • 返回的 lambda 表达式中,c1c2数组中的元素,keyExtractor.apply(c1)得到“键”,然后进行compareTo

comparing源码如下(其中的另一个版本):

public static <T, U> Comparator<T> comparing(
            Function<? super T, ? extends U> keyExtractor,
            Comparator<? super U> keyComparator)
{
    Objects.requireNonNull(keyExtractor);
    Objects.requireNonNull(keyComparator);
    return (Comparator<T> & Serializable)
        (c1, c2) -> keyComparator.compare(keyExtractor.apply(c1),
                                          keyExtractor.apply(c2));
}

这个版本的方法允许我们提供自己版本的Comparator作用于“键”,而不是使用“键”的compareTo方法。如:

Person[] people = new Person[16];
// fill people array
Arrays.sort(people, Comparator.comparing(Person::getName, Comparator.comparingInt(String::length)));

这里不是使用StringcompareTo,而是使用String获取length,然后使用integercompareTo,即比较的”不是名字“,而是”名字的长度”。

除此之外,comparing还有变体形式comparingIntcomparingLongcomparingDouble避免装箱。如:

Person[] people = new Person[16];
// fill people array
Arrays.sort(people, Comparator.comparingInt(p -> p.getName().length()));

thenComparing

实例方法thenComparing用于在使用它的比较器对象在比较时,出现相等时,应该再用什么比较器对象进行比较。如:

Person[] people = new Person[16];
// fill people array
Arrays.sort(people, Comparator.comparing(Person::getLastName).thenComparing(Person::getFirstName));
Arrays.sort(people, Comparator.comparing(Person::getName).thenComparing(Comparator.reverseOrder()));

thenComparing可以直接接受一个“取键函数”,源代码中会将其转变为比较器(即comparing(keyExtractor))。也可以直接提供一个比较器。

上述的第一个例子,提供“取键函数”,实现先比较“姓”,一样时比较“名”。

第二个例子,将people按照名字排序,如果名字相同,则按照当前“自然序”的倒序排序。

thenComparing源码如下(其中的一个版本):

default Comparator<T> thenComparing(Comparator<? super T> other) {
    Objects.requireNonNull(other);
    return (Comparator<T> & Serializable) (c1, c2) -> {
        int res = compare(c1, c2);
        return (res != 0) ? res : other.compare(c1, c2);
    };
}
  • thenComparing的使用会有三个Comparator对象,一个是调用thenComparing方法的所属对象 A,一个是thenComparing方法参数的对象 B,还有一个是thenComparing方法的返回对象 C。

    sort方法使用的是对象 C,调用的compare方法为上述源码中的 lambda 表达式,而该 lambda 表达式也调用了两个compare方法。需要注意的是,这两个方法一个是 A 的,一个是 B 的(不要看成都是 C 的,compare是接口的抽象方法需要具体实现)。虽然 A、B、C 都是Comparator,但它们的定义并不一定相同。

thenComparing方法的另一个版本:

default <U> Comparator<T> thenComparing(
        Function<? super T, ? extends U> keyExtractor,
        Comparator<? super U> keyComparator)
{
    return thenComparing(comparing(keyExtractor, keyComparator));
}

也允许我们指定的比较器比较“键”。

thenComparing也变体形式thenComparingIntthenComparingLongthenComparingDouble来避免装箱。

nullsFirst 和 nullsLast

如果“取键函数”返回null,那么对null对象调用方法就会出现异常,所以我们需要考虑“键”为null的情况。

静态方法nullsFirstnullsLast可以封装Comparator对象,使其在进行null比较时,将其放在数组头或数组尾。

nullsFirstcompare执行的源代码如下。它会检查比较对象是否为null,都不为null时才会执行被封装Comparator对象的compare方法。

@Override
public int compare(T a, T b) {
    if (a == null) {
        return (b == null) ? 0 : (nullFirst ? -1 : 1);
    } else if (b == null) {
        return nullFirst ? 1: -1;
    } else {
        return (real == null) ? 0 : real.compare(a, b);
    }
}

natureOrder 和 reversedOrder

两者均为静态方法分别提供自然顺序及其逆序

reversed

实例方法reversed返回当前Comparator比较的倒序版本。不过,需要注意在方法链中使用时的泛型约束如以下例子:

String[] strings = new String[10];
// fill strings array

Arrays.sort(strings, Comparator.naturalOrder());
Arrays.sort(strings, Comparator.reverseOrder());

Arrays.sort(strings, Comparator.naturalOrder().reversed());							// compile error
Arrays.sort(strings, Comparator.<String>naturalOrder().reversed());

Arrays.sort(strings, Comparator.comparingInt(String::length).reversed());
Arrays.sort(strings, Comparator.comparingInt(s -> s.length()).reversed());			// compile error
Arrays.sort(strings, Comparator.comparingInt((String s) -> s.length()).reversed());

上述例子中有两个例子出现了编译错误。这些调用的第二个方法需要Comparator<String>naturalOrderreverseOrderreversed都返回Comparator<T>,那么为什么reversed()方法出错了且只有它出错?

reversed是实例方法,方法定义中的泛型T是类Comparator<T>的泛型。

public interface Comparator<T> {
    Comparator<T> reversed() {}
    public static <T extends Comparable<? super T>> Comparator<T> naturalOrder() {}
    public static <T extends Comparable<? super T>> Comparator<T> reverseOrder() {}
}

需要naturalOrderreverseOrder提供一个Comparator<String>,这两个方法是静态方法,其泛型是方法的泛型而不是类的。它的泛型判定可以直接根据需求(即Comparator<String>)得出。

静态方法无法使用类的泛型,因为类的泛型要在实例化时确定,而静态方法在类的实例化之前加载,它并不知道类的泛型是啥。

reversed实例方法,它的泛型是类的泛型Java 没有办法通过方法链来推断泛型(目前,未来可能会)。naturalOrder返回了一个Comparator<T>对象,它期望通过上下文(给它具体的类型,如把它赋给某个具体变量)来推断它的类型,但这里只用调用方法,这里就“断”了。需要的Comparator<String>没办法通过reversed方法传递给naturalOrder。在这里,可以明确指出T类型来帮助 Java 类型推断,如上述例子中的Comparator.<String>naturalOrder().reversed()Comparator.comparingInt((String s) -> s.length()).reversed()。它们都明确指出了调用reversed方法的对象的类泛型为Stringreversed方法的返回值就会是Comparator<String>类型。