7.1 文件处理

问题

用户希望使用流来处理文本文件的内容。

方案

使用 java.io.BufferedReaderjava.nio.file.Files 类定义的静态方法 lines,以流的形式返回文件内容。

讨论

在所有基于 FreeBSD 的 UNIX 系统(包括 macOS)中,/usr/share/dict/ 文件夹都包含《韦氏国际英语词典(第 2 版)》。web2 文件收录了大约 23 万个单词,每个单词在文件中占据一行。

假设我们希望查找词典中最长的 10 个单词。为此,我们首先使用 Files.lines 方法,将单词作为字符串流进行检索,然后执行 mapfilter 等常规的流处理操作。相关示例如例 7-1 所示。

例 7-1 在 web2 文件中查找最长的 10 个单词

  1. try (Stream<String> lines = Files.lines(Paths.get("/usr/share/dict/web2")) {
  2. lines.filter(s -> s.length() > 20)
  3. .sorted(Comparator.comparingInt(String::length).reversed())
  4. .limit(10)
  5. .forEach(w -> System.out.printf("%s (%d)%n", w, w.length()));
  6. } catch (IOException e) {
  7. e.printStackTrace();
  8. }

在本例中,filter 方法中的谓词筛掉了长度不足 20 个字符的单词,sorted 方法按长度对这些单词做降序排序,limit 方法在获取前 10 个单词后终止程序,然后打印这些单词。由于流是在 try-with-resources 代码块中打开的,当 try 代码块完成时,系统将自动关闭流与词典文件。

流与 AutoCloseable 接口

Stream 接口继承自 BaseStream,而它是 AutoCloseable 的子接口。因此,可以在 Java 7 新增的 try-with-resources 代码块中使用流。try 代码块执行完毕后,系统将自动调用 close 方法。它不仅会关闭流,还会调用流的流水线(stream pipeline)中的任何 close 处理程序以释放资源。

到目前为止,我们尚未接触 try-with-resources 包装器,因为之前讨论的流是从集合或在内存中生成的。而在本范例中,流是基于文件的,因此 try-with-resources 能确保词典文件也被关闭。

执行例 7-1 中的代码,结果如例 7-2 所示。

例 7-2 词典中最长的 10 个单词

  1. formaldehydesulphoxylate (24)
  2. pathologicopsychological (24)
  3. scientificophilosophical (24)
  4. tetraiodophenolphthalein (24)
  5. thyroparathyroidectomize (24)
  6. anthropomorphologically (23)
  7. blepharosphincterectomy (23)
  8. epididymodeferentectomy (23)
  9. formaldehydesulphoxylic (23)
  10. gastroenteroanastomosis (23)

如上所示,词典中有 5 个单词的长度为 24 个字符。结果之所以按字母顺序显示,只是因为原始文件中的单词是按字母顺序排序的。在例 7-1 中,如果为 sorted 方法的 Comparator 参数添加一个 thenComparing 子句,就能调整等长单词的排序方式了。

在 5 个长度为 24 个字符的单词之后,是 5 个长度为 23 个字符的单词,其中大部分单词来自医学领域。3

3好在 blepharosphincterectomy(眼轮匝肌切除术)与其字面意思无关,这是一个与减轻角膜上眼睑压力有关的单词。听起来很头疼?其实可能更头疼。

如果将 Collectors.counting 作为下游收集器,就能确定词典中每种长度的单词数量,如例 7-3 所示。

例 7-3 确定每种长度的单词数量(升序排序)

  1. try (Stream<String> lines = Files.lines(Paths.get("/usr/share/dict/web2"))) {
  2. lines.filter(s -> s.length() > 20)
  3. .collect(Collectors.groupingBy(String::length, Collectors.counting()))
  4. .forEach((len, num) -> System.out.println(len + ": " + num));
  5. }

上述代码使用收集器 groupingBy 创建一个 Map,其中键为单词长度,值为每种长度的单词数量。代码的执行结果如下:

  1. 21: 82
  2. 22: 41
  3. 23: 17
  4. 24: 5

上述输出虽然提供了部分信息,但并非特别有用。且结果按升序排序,这也可能不满足我们的要求。

另一种方案是采用 Map.Entry 接口新增的静态方法 comparingByKeycomparingByValue,二者均传入可选的 Comparator(相关讨论请参见范例 4.4)。如例 7-4 所示,通过比较器 reverseOrder 进行排序时,将返回自然顺序的相反顺序。

例 7-4 确定每种长度的单词数量(降序排序)

  1. try (Stream<String> lines = Files.lines(Paths.get("/usr/share/dict/web2"))) {
  2. Map<Integer, Long> map = lines.filter(s -> s.length() > 20)
  3. .collect(Collectors.groupingBy(String::length, Collectors.counting()));
  4.  
  5. map.entrySet().stream()
  6. .sorted(Map.Entry.comparingByKey(Comparator.reverseOrder()))
  7. .forEach(e -> System.out.printf("Length %d: %d words%n",
  8. e.getKey(), e.getValue()));
  9. }

程序的执行结果如下:

  1. Length 24: 5 words
  2. Length 23: 17 words
  3. Length 22: 41 words
  4. Length 21: 82 words

如果数据源不是文件,也可以使用 BufferedReader 类新增的 lines 方法(这种情况下,lines 是一个实例方法)。采用 BufferedReader.lines 方法对例 7-4 改写,结果如例 7-5 所示。

例 7-5 BufferedReader.lines 方法的应用

  1. try (Stream<String> lines =
  2. new BufferedReader(
  3. new FileReader("/usr/share/dict/words")).lines()) {
  4.  
  5. // 其余代码与例7-4相同
  6. }

需要再次强调的是,由于 Stream 接口实现了 AutoCloseable 接口,当 try-with-resources 代码块关闭流时,底层 BufferedReader 也随之关闭。

另见

有关对映射排序的讨论请参见范例 4.4。