现代 Java 中的常见 I/O 任务

此页面由 Cay HorstmannUPL 下贡献

 

介绍

本文重点介绍应用程序程序员可能遇到的任务,尤其是在 Web 应用程序中,例如

  • 读取和写入文本文件
  • 从 Web 读取文本、图像、JSON
  • 访问目录中的文件
  • 读取 ZIP 文件
  • 创建临时文件或目录

Java API 支持许多其他任务,这些任务在 Java I/O API 教程 中有详细说明。

本文重点介绍自 Java 8 以来 API 的改进。特别是

 

读取文本文件

您可以像这样将文本文件读取到字符串中

String content = Files.readString(path);

这里,pathjava.nio.Path 的实例,可以通过以下方式获得

var path = Path.of("/usr/share/dict/words");

在 Java 18 之前,强烈建议您在任何读取或写入字符串的文件操作中指定字符编码。如今,最常见的字符编码是 UTF-8,但为了向后兼容,Java 使用了“平台编码”,这在 Windows 上可能是旧版编码。为了确保可移植性,文本 I/O 操作需要参数 StandardCharsets.UTF_8。这不再是必需的。

如果您希望将文件作为一系列行,请调用

List<String> lines = Files.readAllLines(path);

如果文件很大,请将行作为 Stream<String> 延迟处理

try (Stream<String> lines = Files.lines(path)) {
    . . .
}

如果您可以使用流操作(例如 mapfilter)自然地处理行,也使用 Files.lines。请注意,Files.lines 返回的流需要关闭。为了确保这一点,请使用try-with-resources 语句,如前面的代码片段所示。

现在没有充分的理由使用 readLine 方法 java.io.BufferedReader

要将输入拆分为除行以外的其他内容,请使用 java.util.Scanner。例如,以下是如何读取由非字母分隔的单词

Stream<String> tokens = new Scanner(path).useDelimiter("\\PL+").tokens();

Scanner 类还具有读取数字的方法,但通常将输入读取为每行一个字符串或单个字符串,然后解析它更简单。

从文本文件解析数字时要小心,因为它们的格式可能与区域设置相关。例如,输入 100.000 在美国区域设置中是 100.0,而在德国区域设置中是 100000.0。使用 java.text.NumberFormat 进行区域设置特定的解析。或者,您可以使用 Integer.parseInt/Double.parseDouble

 

写入文本文件

您可以使用单个调用将字符串写入文本文件

String content = . . .;
Files.writeString(path, content);

如果您有一个行列表而不是单个字符串,请使用

List<String> lines = . . .;
Files.write(path, lines);

对于更通用的输出,如果您想使用 printf 方法,请使用 PrintWriter

var writer = new PrintWriter(path.toFile());
writer.printf(locale, "Hello, %s, next year you'll be %d years old!%n", name, age + 1);

请注意,printf 是区域设置特定的。写入数字时,请确保以适当的格式写入它们。不要使用 printf,请考虑使用 java.text.NumberFormatInteger.toString/Double.toString

奇怪的是,截至 Java 21,没有 PrintWriter 构造函数具有 Path 参数。

如果您不使用 printf,您可以使用 BufferedWriter 类并使用 write 方法写入字符串。

var writer = Files.newBufferedWriter(path);
writer.write(line); // Does not write a line separator
writer.newLine(); 

请记住,在完成时关闭 writer

 

从输入流读取

使用流最常见的原因可能是从网站读取内容。

如果您需要设置请求标头或读取响应标头,请使用 HttpClient

HttpClient client = HttpClient.newBuilder().build();
HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create("https://horstmann.com/index.html"))
    .GET()
    .build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
String result = response.body();

如果您只需要数据,这太过分了。相反,请使用

InputStream in = new URI("https://horstmann.com/index.html").toURL().openStream();

然后将数据读取到字节数组中,并选择性地将其转换为字符串

byte[] bytes = in.readAllBytes();
String result = new String(bytes);

或将数据传输到输出流

OutputStream out = Files.newOutputStream(path);
in.transferTo(out);

请注意,如果您只想读取输入流的所有字节,则不需要循环。

但是您真的需要输入流吗?许多 API 允许您从文件或 URL 读取。

您最喜欢的 JSON 库可能具有从文件或 URL 读取的方法。例如,使用 Jackson jr

URL url = new URI("https://dog.ceo/api/breeds/image/random").toURL();
Map<String, Object> result = JSON.std.mapFrom(url);

以下是如何从前面的调用中读取狗的图像

URL url = new URI(result.get("message").toString()).toURL();
BufferedImage img = javax.imageio.ImageIO.read(url);

这比将输入流传递给 read 方法更好,因为该库可以使用来自 URL 的附加信息来确定图像类型。

 

Files API

java.nio.file.Files 类提供了一套全面的文件操作,例如创建、复制、移动和删除文件和目录。文件系统基础 教程提供了详细的说明。在本节中,我重点介绍了一些常见任务。

遍历目录和子目录中的条目

对于大多数情况,您可以使用两种方法之一。 Files.list 方法访问目录中的所有条目(文件、子目录、符号链接)。

try (Stream<Path> entries = Files.list(pathToDirectory)) {
    . . .
}

使用try-with-resources语句确保跟踪迭代的流对象将被关闭。

如果您还想访问后代目录的条目,请改用方法 Files.walk

Stream<Path> entries = Files.walk(pathToDirectory);

然后只需使用流方法来定位您感兴趣的条目,并收集结果

try (Stream<Path> entries = Files.walk(pathToDirectory)) {
    List<Path> htmlFiles = entries.filter(p -> p.toString().endsWith("html")).toList();
    . . .
}

以下是遍历目录条目的其他方法

使用 ZIP 文件

从 Java 1.1 开始,ZipInputStreamZipOutputStream 类提供了一个用于处理 ZIP 文件的 API。但 API 有点笨拙。Java 8 引入了一个更好的ZIP 文件系统

try (FileSystem fs = FileSystems.newFileSystem(pathToZipFile)) {
    . . .
}

try-with-resources语句确保在 ZIP 文件操作后调用 close 方法。该方法更新 ZIP 文件以反映文件系统中的任何更改。

然后您可以使用 Files 类的 方法。这里我们获取 ZIP 文件中所有文件的列表

try (Stream<Path> entries = Files.walk(fs.getPath("/"))) {
    List<Path> filesInZip = entries.filter(Files::isRegularFile).toList();
}

要读取文件内容,只需使用 Files.readStringFiles.readAllBytes

String contents = Files.readString(fs.getPath("/LICENSE"));

您可以使用 Files.delete 删除文件。要添加或替换文件,只需使用 Files.writeStringFiles.write

创建临时文件和目录

我经常需要收集用户输入,生成文件并运行外部进程。然后我使用临时文件,这些文件在下次重启后消失,或者使用一个临时目录,我在进程完成后擦除。

我使用两种方法 Files.createTempFileFiles.createTempDirectory 来实现这一点。

Path filePath = Files.createTempFile("myapp", ".txt");
Path dirPath = Files.createTempDirectory("myapp");

这将在合适的位置(Linux 中的 /tmp)创建具有给定前缀的临时文件或目录,对于文件,则具有后缀。

 

结论

网络搜索和 AI 聊天可能会为常见的 I/O 操作建议不必要的复杂代码。通常有更好的选择

  1. 您不需要循环来读取或写入字符串或字节数组。
  2. 您甚至可能不需要流、读取器或写入器。
  3. 熟悉 Files 方法,用于创建、复制、移动和删除文件和目录。
  4. 使用 Files.listFiles.walk 遍历目录条目。
  5. 使用 ZIP 文件系统来处理 ZIP 文件。
  6. 远离传统的 File 类。

上次更新: 2024 年 4 月 24 日


返回教程列表