4.4 对映射排序
问题
用户希望根据键或值对 Map 排序。
方案
使用 Map.Entry 接口新增的静态方法。
讨论
Map 接口始终包含一个称为 Map.Entry 的公共静态内部接口(public, static, inner interface),它表示一个键值对。Map 接口定义的 entrySet 方法返回 Map.Entry 元素的 Set。在 Java 8 之前,getKey 和 getValue 是 Map.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
- System.out.println("\nNumber of words of each length:");
- try (Stream<String> lines = Files.lines(dictionary)) {
- lines.filter(s -> s.length() > 20)
- .collect(Collectors.groupingBy(
- String::length, Collectors.counting()))
- .forEach((len, num) -> System.out.printf("%d: %d%n", len, num));
- } catch (IOException e) {
- e.printStackTrace();
- }
本例的详细讨论请参见范例 7.1,目前可以这样理解。
- 文件在
try-with-resources代码块内读取。由于Stream接口实现了AutoCloseable,try代码块执行完毕后,Java 对Stream调用close方法,然后对File调用close方法。 - 筛选器只筛出长度至少为 20 个字符的单词,以供进一步处理。
Collectors.groupingBy方法传入Function作为第一个参数,表示分类器(classifier)。在本例中,分类器是每个字符串的长度。如果groupingBy方法只传入一个参数,则结果为Map,其中键为分类器的值,值为匹配分类器的元素列表。这种情况下,groupingBy(String::length)将返回Map,其中键为单词长度,值为该长度的单词列表。> - 在本例中,双参数形式的
groupingBy方法传入另一个Collector,它称为下游收集器(downstream collector),用于对单词列表进行后期处理。这种情况下,groupingBy方法的返回类型是Map,其中键为单词长度,值为词典中该长度的单词数量。
例 4-18 的输出结果如下:
Number of words of each length:21: 8222: 4123: 1724: 5
换言之,有 82 个单词的长度为 21,41 个单词的长度为 22,17 个单词的长度为 23,5 个单词的长度为 243。
3根据记录,这 5 个最长的单词为 formaldehydesulphoxylate(甲醛次硫酸氢钠)、pathologicopsychological(病理心理学)、scientificophilosophical(科学哲学)、tetraiodophenolphthalein(四碘酚酞)以及 thyroparathyroidectomize(甲状腺甲状腺切除术)。希望拼写检查工具可以识别这些单词。
不难看到,程序按单词长度的升序打印映射中的单词。如果希望按降序打印,可以使用 Map.Entry 接口定义的 comparingByKey 方法,如例 4-19 所示。
例 4-19 根据键对映射排序
- System.out.println("\nNumber of words of each length (desc order):");
- try (Stream<String> lines = Files.lines(dictionary)) {
- Map<Integer, Long> map = lines.filter(s -> s.length() > 20)
- .collect(Collectors.groupingBy(
- String::length, Collectors.counting()));
- map.entrySet().stream()
- .sorted(Map.Entry.comparingByKey(Comparator.reverseOrder()))
- .forEach(e -> System.out.printf("Length %d: %2d words%n",
- e.getKey(), e.getValue()));
- } catch (IOException e) {
- e.printStackTrace();
- }
返回 Map 之后,程序将提取 entrySet 并产生一个流。Stream.sorted 方法使用提供的比较器生成经过排序的流。
在本例中,comparingByKey 方法返回一个根据键进行排序的比较器。如果希望以键的相反顺序排序,可以使用 comparingByKey 方法的重载形式,它传入比较器作为参数。
![]()
Stream.sorted方法生成一个新的排序流,它不对源数据进行修改。换言之,原始Map不受影响。
例 4-19 的输出结果如下:
Number of words of each length (desc order):Length 24: 5 wordsLength 23: 17 wordsLength 22: 41 wordsLength 21: 82 words
表 4-1 列出的 comparingByValue 方法,用法与 comparingByKey 类似。
另见
有关根据键或值对 Map 进行排序的其他示例请参见附录 A,有关下游收集器的讨论请参见范例 4.6,有关词典中的文件处理请参见范例 7.1。
