4.4 对映射排序

问题

用户希望根据键或值对 Map 排序。

方案

使用 Map.Entry 接口新增的静态方法。

讨论

Map 接口始终包含一个称为 Map.Entry 的公共静态内部接口(public, static, inner interface),它表示一个键值对。Map 接口定义的 entrySet 方法返回 Map.Entry 元素的 Set。在 Java 8 之前,getKeygetValueMap.Entry 接口两种最常用的方法,二者分别返回与某个条目对应的键和值。

Java 8 为 Map.Entry 接口引入了一些新的静态方法,如表 4-1 所示。

表4-1 Map.Entry接口新增的静态方法(参见Java 8文档)

方法 描述
comparingByKey() 返回一个比较器,它根据键的自然顺序比较 Map.Entry
comparingByKey(Comparator cmp) 返回一个比较器,它使用给定的 Comparator 并根据键比较 Map.Entry
comparingByValue() 返回一个比较器,它根据值的自然顺序比较 Map.Entry
comparingByValue(Comparator cmp) 返回一个比较器,它使用给定的 Comparator 并根据值比较 Map.Entry

我们以创建单词长度与单词数量的 Map 为例,演示上述方法的用法(例 4-18)。所有 Unix 系统的 usr/share/dict/words 目录中都包含一个文件,它收录了《韦氏词典(第 2 版)》的内容,每个单词在文件中占据一行。Files.lines 方法可用于读取文件并生成一个包含这些行的字符串流。此时,流包含词典中的所有单词。

例 4-18 将词典文件读入 Map

  1. System.out.println("\nNumber of words of each length:");
  2. try (Stream<String> lines = Files.lines(dictionary)) {
  3. lines.filter(s -> s.length() > 20)
  4. .collect(Collectors.groupingBy(
  5. String::length, Collectors.counting()))
  6. .forEach((len, num) -> System.out.printf("%d: %d%n", len, num));
  7. } catch (IOException e) {
  8. e.printStackTrace();
  9. }

本例的详细讨论请参见范例 7.1,目前可以这样理解。

  • 文件在 try-with-resources 代码块内读取。由于 Stream 接口实现了 AutoCloseabletry 代码块执行完毕后,Java 对 Stream 调用 close 方法,然后对 File 调用 close 方法。
  • 筛选器只筛出长度至少为 20 个字符的单词,以供进一步处理。
  • Collectors.groupingBy 方法传入 Function 作为第一个参数,表示分类器(classifier)。在本例中,分类器是每个字符串的长度。如果 groupingBy 方法只传入一个参数,则结果为 Map,其中键为分类器的值,值为匹配分类器的元素列表。这种情况下,groupingBy(String::length) 将返回 Map>,其中键为单词长度,值为该长度的单词列表。
  • 在本例中,双参数形式的 groupingBy 方法传入另一个 Collector,它称为下游收集器(downstream collector),用于对单词列表进行后期处理。这种情况下,groupingBy 方法的返回类型是 Map,其中键为单词长度,值为词典中该长度的单词数量。

例 4-18 的输出结果如下:

  1. Number of words of each length:
  2. 21: 82
  3. 22: 41
  4. 23: 17
  5. 24: 5

换言之,有 82 个单词的长度为 21,41 个单词的长度为 22,17 个单词的长度为 23,5 个单词的长度为 243。

3根据记录,这 5 个最长的单词为 formaldehydesulphoxylate(甲醛次硫酸氢钠)、pathologicopsychological(病理心理学)、scientificophilosophical(科学哲学)、tetraiodophenolphthalein(四碘酚酞)以及 thyroparathyroidectomize(甲状腺甲状腺切除术)。希望拼写检查工具可以识别这些单词。

不难看到,程序按单词长度的升序打印映射中的单词。如果希望按降序打印,可以使用 Map.Entry 接口定义的 comparingByKey 方法,如例 4-19 所示。

例 4-19 根据键对映射排序

  1. System.out.println("\nNumber of words of each length (desc order):");
  2. try (Stream<String> lines = Files.lines(dictionary)) {
  3. Map<Integer, Long> map = lines.filter(s -> s.length() > 20)
  4. .collect(Collectors.groupingBy(
  5. String::length, Collectors.counting()));
  6.  
  7. map.entrySet().stream()
  8. .sorted(Map.Entry.comparingByKey(Comparator.reverseOrder()))
  9. .forEach(e -> System.out.printf("Length %d: %2d words%n",
  10. e.getKey(), e.getValue()));
  11. } catch (IOException e) {
  12. e.printStackTrace();
  13. }

返回 Map 之后,程序将提取 entrySet 并产生一个流。Stream.sorted 方法使用提供的比较器生成经过排序的流。

在本例中,comparingByKey 方法返回一个根据键进行排序的比较器。如果希望以键的相反顺序排序,可以使用 comparingByKey 方法的重载形式,它传入比较器作为参数。

4.4 对映射排序 - 图1 Stream.sorted 方法生成一个新的排序流,它不对源数据进行修改。换言之,原始 Map 不受影响。

例 4-19 的输出结果如下:

  1. Number of words of each length (desc order):
  2. Length 24: 5 words
  3. Length 23: 17 words
  4. Length 22: 41 words
  5. Length 21: 82 words

表 4-1 列出的 comparingByValue 方法,用法与 comparingByKey 类似。

另见

有关根据键或值对 Map 进行排序的其他示例请参见附录 A,有关下游收集器的讨论请参见范例 4.6,有关词典中的文件处理请参见范例 7.1。