Java8特性尝鲜之集合排序

Published: 03 Mar 2014 Category: Java

只要提到Java8的lambda表达式,数组或者集合的排序都是一个非常棒的例子,这是因为自从Java1.2以来,只要一提到排序,Comparator这个接口总是挥之不去。有了Java8后,在很多排序中,Comparator都可以使用lambda表达式来替换了。

在接下来的例子中,我们会用到这个简单的Person类:

static class Person {
    final String firstName;
    final String lastName;
 
    Person(String firstName, String lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }
 
    @Override
    public String toString() {
        return "Person{" +
                "firstName='" + firstName + '\'' +
                ", lastName='" + lastName + '\'' +
                '}';
    }
}

很明显,只要我们让Person类实现Comparable接口就可以让它支持排序了,不过我们现在考虑的是使用外部的Comparator进行排序的情况。来看下下面这个Person列表,其中名字是由某个在线随机名字生成器生成的:

List<Person> people =
Arrays.asList(
    new Person("Jane", "Henderson"),
    new Person("Michael", "White"),
    new Person("Henry", "Brighton"),
    new Person("Hannah", "Plowman"),
    new Person("William", "Henderson")
);

我们希望是先根据姓氏排序,然后根据名字排序。

Java 7中的排序

使用Comparator的一个经典的Java 7的例子是:

people.sort(new Comparator<Person>() {
  @Override
  public int compare(Person o1, Person o2) {
    int result = o1.lastName.compareTo(o2.lastName);
 
    if (result == 0)
      result = o1.firstName.compareTo(o2.firstName);
 
    return result;
  }
});
people.forEach(System.out::println);

上述代码的输出结果是:

Person{firstName='Henry', lastName='Brighton'}
Person{firstName='Jane', lastName='Henderson'}
Person{firstName='William', lastName='Henderson'}
Person{firstName='Hannah', lastName='Plowman'}
Person{firstName='Michael', lastName='White'}
Java 8的排序

现在我们来把上面排序的代码改成Java8的风格:

Comparator<Person> c = (p, o) ->
    p.lastName.compareTo(o.lastName);
 
c = c.thenComparing((p, o) ->
    p.firstName.compareTo(o.firstName));
 
people.sort(c);
people.forEach(System.out::println);

结果当然是一样的。上面的代码是什么意思?首先,我们将一个lambda表达式赋值给一个Person的Comparator的本地变量:

Comparator<Person> c = (p, o) ->
    p.lastName.compareTo(.lastName);

不像Scala,c#或者Ceylon这些语言,他们可以从表达式中进行类型推导,本地变量的声明只需要用一个val关键字(或者类似的)就可以了,而Java是从变量声明到赋值的表达式进行推导的。

简单的话,可以这么说,类型推导是从左到右,而不是从右到左的。这样让Comparator显得有点笨重,因为Java不能当参数传递到sort方法时才做类型推导。

一旦我们把Comparator赋值给一个变量后,我们可以通过thenComparing方法很方便的把别的比较器增加到这个比较链中:

c = c.thenComparing((p, o) ->
    p.firstName.compareTo(o.firstName));

最后,我们把这个Comparator传给列表的新的sort方法,它是List接口里面默认实现的一个方法:

default void sort(Comparator<? super E> c) {
    Collections.sort(this, c);
}

上述局限性的一个解决方法

虽然Java类型推导的这个小缺陷令人有点沮丧,但是我们可以通过创建一个泛型的IdentityComparator来解决这个问题:

class Utils {
    static <E> Comparator<E> compare() {
        return (e1, e2) -> 0;
    }
}

使用上面这个compare方法,我们就可以很流畅地完成一个比较器链的编写了:

people.sort(
    Utils.<Person>compare()
         .thenComparing((p, o) ->
              p.lastName.compareTo(o.lastName))
         .thenComparing((p, o) ->
              p.firstName.compareTo(o.firstName))
);
 
people.forEach(System.out::println);
提取关键字

我们还可以进一步改进。因为我们一般只使用Comparator参数的POJO/DTO对象的同样一个值进行比较,我们用一个"提取关键值"的函数把它们传给这些新的API。下面是示例:

people.sort(Utils.<Person>compare()
      .thenComparing(p -> p.lastName)
      .thenComparing(p -> p.firstName));
people.forEach(System.out::println);

对于一个指定的Person对象,我们给接口提供一个提取的函数,比如,p.lastname。事实上,一旦我们使用了关键值提取器,我们可以省略上面的那个帮助方法,因为库里面也提供了一个comparing的方法来初始化整个调用链:

people.sort(
    Comparator.comparing((Person p) -> p.lastName)
          .thenComparing(p -> p.firstName));
people.forEach(System.out::println);

同样的,由于编译器无法进行类型推导,我们告诉它具体的类型,尽管在这个例子中sort方法已经提供了足够的信息了。想了解更多关于Java8的泛型推导的资料,请看下我们之前发的一些文章。

结论

跟Java5一样,升级最大的提升主要体现在JDK中。Java5给Comparator带来的是类型安全,而Java8则是让我们能更方便的使用它们。

原创文章转载请注明出处:Java8特性尝鲜之集合排序

英文原文链接