1. 概述

有时,我们需要从文件中读取原始文本,并清理混乱的内容,例如移除换行符。

在这个教程中,我们将探讨在Java中从文件中移除换行符的不同方法。

2. 换行符的简要说明

在深入代码之前,让我们快速了解一下我们想要移除的目标对象:换行符。

乍看之下,这很简单。换行符就是一个分隔行的字符。然而,实际上有不同类型的换行符。如果我们处理不当,可能会遇到陷阱。一个例子可以解释清楚:

假设我们有两个文本文件,multiple-line-1.txtmultiple-line-2.txt。我们称它们为 file1file2。如果我们在IDE(如 IntelliJ)的编辑器中打开它们,看起来是这样的:

A,
 B,
 C,
 D,
 E,
 F

我们可以看到,每个文件都有六行,从第二行开始,每行都有一个空格字符。 所以我们认为 file1file2 包含完全相同的文本。

但是,现在我们使用 cat 命令(带有 -n 显示行号和 -e 显示非打印字符选项)打印文件内容:

$ cat -ne multiple-line-1.txt
     1  A,$
     2   B,$
     3   C,$
     4   D,$
     5   E,$
     6   F$

file1 的输出与我们在IntelliJ编辑器中看到的一样。但 file2 的显示却大不相同:

$ cat -ne multiple-line-2.txt
     1  A,^M B,$
     2   C,$
     3   D,^M E,$
     4   F$

这是因为存在三种不同的换行符:

  • \r(回车),Mac OS X之前的行结束符
  • \n(换行),在nix和Mac OS中使用的行结束符
  • \r\n(CRLF),Windows中的行结束符

cat -e 显示CRLF为*^M*。所以,我们看到 file2 包含CRLF。可能这个文件是在Windows上创建的。根据需求,我们可能希望移除所有类型的换行符,或者只移除当前系统的换行符。

接下来,我们将用这两个文件作为示例,看看如何读取它们的内容并移除换行符。为了简化,我们将创建两个辅助方法来返回每个文件的 [Path](/java-path-vs-file#javaniofilepath-class)

Path file1Path() throws Exception {
    return Paths.get(this.getClass().getClassLoader().getResource("multiple-line-1.txt").toURI());
} 

Path file2Path() throws Exception {
    return Paths.get(this.getClass().getClassLoader().getResource("multiple-line-2.txt").toURI());
}

请注意,本文中使用的策略需要将整个文本读入内存,因此请考虑处理非常大的文件

3. 使用 line.separator 替换空字符串

系统属性 line.separator 存储了特定于当前操作系统的行分隔符。 因此,如果我们只想移除特定于当前系统的换行符,我们可以将 line.separator 替换为空字符串。例如,在Linux机器上,这种方法可以移除 file1 中的所有换行符:

String content = Files.readString(file1Path(), StandardCharsets.UTF_8);

String result = content.replace(System.getProperty("line.separator"), "");
assertEquals("A, B, C, D, E, F", result);

我们使用 Files 类的 readString() 方法将文件内容加载到字符串中,然后通过 replace() 方法应用替换。

然而,同样的方法无法移除 file2 中的所有换行符,因为它包含CRLF换行符:

String content = Files.readString(file2Path(), StandardCharsets.UTF_8);

String result = content.replace(System.getProperty("line.separator"), "");
assertNotEquals("A, B, C, D, E, F", result); // <-- NOT equals assertion!

接下来,我们将查看是否可以独立于系统地移除所有换行符。

4. 使用正则表达式替换\n\r

我们已经了解到三种不同的换行符涵盖了\n\r字符。因此,如果我们想独立于系统地移除所有换行符,我们可以将\n\r替换为空字符串

String content1 = Files.readString(file1Path(), StandardCharsets.UTF_8);

// file contains CRLF
String content2 = Files.readString(file2Path(), StandardCharsets.UTF_8);

String result1 = content1.replace("\r", "").replace("\n", "");
String result2 = content2.replace("\r", "").replace("\n", "");

assertEquals("A, B, C, D, E, F", result1);
assertEquals("A, B, C, D, E, F", result2);

当然,我们也可以使用基于正则表达式的 replaceAll() 方法实现同样的目标。以 file2 为例,看看它是如何工作的:

String resultReplaceAll = content2.replaceAll("[\\n\\r]", "");
assertEquals("A, B, C, D, E, F", resultReplaceAll);

5. 使用 readAllLines() 后再使用 join()

回顾目前为止学到的方法,我们首先从文件中读取全部内容,然后替换 line.separator 系统属性或\n\r字符为空。这些方法的共同点是我们自己手动管理行分隔符。

Files 类提供了 readAllLines() 方法,将文件内容读取为行并返回一个字符串列表。值得注意的是,readAllLines() 将所有提到的三种换行符视为一行分隔符。换句话说,这个方法会移除输入中的所有换行符。我们需要做的是连接返回列表中的元素

join() 方法非常方便,可以连接列表或字符串数组:

List<String> lines1 = Files.readAllLines(file1Path(), StandardCharsets.UTF_8);

// file contains CRLF
List<String> lines2 = Files.readAllLines(file2Path(), StandardCharsets.UTF_8);

String result1 = String.join("", lines1);
String result2 = String.join("", lines2);

assertEquals("A, B, C, D, E, F", result1);
assertEquals("A, B, C, D, E, F", result2);

6. 总结

在这篇文章中,我们首先讨论了不同类型的换行符。然后,我们探索了从文件中移除换行符的不同方法。

一如既往,示例的完整源代码可以在GitHub上找到。