4.1 利用比较器实现排序

问题

用户希望实现对象的排序。

方案

使用传入 ComparatorStream.sorted 方法,Comparator 既可以通过 lambda 表达式实现,也可以使用 Comparator 接口定义的某种 comparing 方法生成。

讨论

Stream.sorted 方法根据类的自然顺序(natural ordering)生成一个新的排序流,自然顺序是通过实现 java.util.Comparable 接口来指定的。

如例 4-1 所示,我们对字符串集合进行排序。

69

例 4-1 根据字典序(自然顺序)对字符串排序

  1. private List<String> sampleStrings =
  2. Arrays.asList("this", "is", "a", "list", "of", "strings");
  3.  
  4. public List<String> defaultSort() {
  5. Collections.sort(sampleStrings);
  6. return sampleStrings;
  7. }
  8.  
  9. public List<String> defaultSortUsingStreams() {
  10. return sampleStrings.stream()
  11. .sorted()
  12. .collect(Collectors.toList());
  13. }

❶ 默认排序(Java 7 及之前的版本)

❷ 默认排序(Java 8 及之后的版本)

从 Java 1.2 引入集合框架(collections framework)开始,工具类 Collections 就已存在。Collections 类定义的静态方法 sort 传入 List 作为参数,但返回 void。这种排序是破坏性的,会修改所提供的集合。换言之,Collections.sort 方法不符合 Java 8 所倡导的将不可变性(immutability)置于首要位置的函数式编程原则。

Java 8 采用 Stream.sorted 方法实现相同的排序,但不对原始集合进行修改,而是生成一个新的流。在例 8-1 中,完成集合的排序后,程序根据类的自然顺序对返回列表进行排序。对于字符串,自然顺序是字典序(lexicographic order);如果所有字符串均为小写,自然顺序就相当于字母顺序(alphabetical order),从本例可以观察到这一点。

如果希望以其他方式排序字符串,可以使用 sorted 方法的重载形式,传入 Comparator 作为参数。

例 4-2 采用两种不同的方式,根据长度对字符串进行排序。

例 4-2 根据长度对字符串排序

  1. public List<String> lengthSortUsingSorted() {
  2. return sampleStrings.stream()
  3. .sorted((s1, s2) -> s1.length() - s2.length())
  4. .collect(toList());
  5. }
  6.  
  7. public List<String> lengthSortUsingComparator() {
  8. return sampleStrings.stream()
  9. .sorted(Comparator.comparingInt(String::length))
  10. .collect(toList());
  11. }

❶ 使用 lambda 表达式作为 Comparator,根据长度进行排序

❷ 使用 Comparator.comparingInt 方法

sorted 方法的参数为 java.util.Comparator,它是一种函数式接口。对于第一个方法 lengthSortUsingSorted,所提供的 lambda 表达式用于实现 Comparator.compare 方法。在 Java 7 及之前的版本中,实现通常由匿名内部类提供,而本例仅需要一个 lambda 表达式。

4.1 利用比较器实现排序 - 图1 Java 8 引入 sort(Comparator) 作为 List 接口的默认实例方法,它相当于 Collections 类的 sort(List, Comparator) 方法。由于两种方法返回 void 且都是破坏性的,本节讨论的 Stream.sorted(Comparator) 方法(返回一个新的排序流)仍然是首选方案。

第二个方法 lengthSortUsingComparator 利用了 Comparator 接口新增的某种静态方法。comparingInt 方法传入一个 ToIntFunction 类型的参数(文档称之为 keyExtractor),用于将字符串转换为整型数据,并生成一个使用该 keyExtractor 对集合进行排序的 Comparator

Comparator 接口新增的各种默认方法极为有用。虽然不难写出一个根据长度进行排序的 Comparator,但如果需要对多个字段排序,代码可能会变得很复杂。例如,我们希望根据长度对字符串排序,如果长度相同则按字母顺序排序。如例 4-3 所示,采用 Comparator 接口提供的默认和静态方法很容易就能解决这个问题。

例 4-3 根据长度对字符串排序,长度相同则按字母顺序排序

  1. public List<String> lengthSortThenAlphaSort() {
  2. return sampleStrings.stream()
  3. .sorted(comparing(String::length)
  4. .thenComparing(naturalOrder()))
  5. .collect(toList());
  6. }

➊ 根据长度对字符串排序,长度相同则按字母顺序排序

Comparator 接口定义了一个称为 thenComparing 的默认方法。与 comparing 方法类似,thenComparing 方法也传入 Function 作为参数,文档同样将其称为 keyExtractor。如果将 thenComparing 方法链接到 comparing 方法会返回 Comparator,它首先比较第一个数量,如果相同则比较第二个数量,以此类推。

静态导入通常使代码更易于阅读。一旦熟悉 Comparator 接口和 Collectors 类定义的各种静态方法,就能写出更简单的代码。在本例中,comparingnaturalOrder 方法已被静态导入。

即便没有实现 Comparable 接口,上述方案也适用于任何类。如例 4-4 所示,我们定义一个描述高尔夫球手的 Golfer 类。

例 4-4 描述高尔夫球手的 Golfer

  1. public class Golfer {
  2. private String first;
  3. private String last;
  4. private int score;
  5.  
  6. // 其他方法
  7. }

为了创建一个高尔夫锦标赛排行榜,可以依次根据各个球手的得分、姓氏、名字进行排序。例 4-5 显示了如何实现高尔夫球手的排序。

例 4-5 对高尔夫球手排序

  1. private List<Golfer> golfers = Arrays.asList(
  2. new Golfer("Jack", "Nicklaus", 68),
  3. new Golfer("Tiger", "Woods", 70),
  4. new Golfer("Tom", "Watson", 70),
  5. new Golfer("Ty", "Webb", 68),
  6. new Golfer("Bubba", "Watson", 70)
  7. );
  8.  
  9. public List<Golfer> sortByScoreThenLastThenFirst() {
  10. return golfers.stream()
  11. .sorted(comparingInt(Golfer::getScore)
  12. .thenComparing(Golfer::getLast)
  13. .thenComparing(Golfer::getFirst))
  14. .collect(toList());
  15. }

调用 sortByScoreThenLastThenFirst 方法,输出如例 4-6 所示。

例 4-6 对高尔夫球手排序后的结果

  1. Golfer{first='Jack', last='Nicklaus', score=68}
  2. Golfer{first='Ty', last='Webb', score=68}
  3. Golfer{first='Bubba', last='Watson', score=70}
  4. Golfer{first='Tom', last='Watson', score=70}
  5. Golfer{first='Tiger', last='Woods', score=70}

本例首先根据得分对各个球手进行排序,因此 Jack Nicklaus 和 Ty Webb1 排在 Tiger Woods、Bubba Watson 与 Tom Watson 之前。得分相同时根据姓氏进行排序,因此 Jack Nicklaus 排在 Ty Webb 之前,Bubba Watson 和 Tom Watson 排在 Tiger Woods 之前。如果得分和姓氏都相同,则根据名字进行排序,因此 Bubba Watson 排在 Tom Watson 之前。

1当然,Ty Webb 是电影《小小球童》中的角色。Judge Smails 问道:“今天打得怎么样?”Ty Webb 回答:“我没有计分。”Smails 继续问道:“那你怎么衡量自己与其他球手的水平呢?”Webb 回答:“身高。”根据身高进行排序留给读者作为简单的练习。

借由 Comparator 接口定义的默认和静态方法以及 Stream 接口新增的 sorted 方法,生成复杂的排序变得易如反掌。