最新Java中的6个I/O操作教程


本文重点介绍应用程序程序员可能遇到的任务,特别是在 Web 应用程序中,例如:
  • 读写文本文件
  • 从网络上读取文本、图像、JSON
  • 访问目录中的文件
  • 读取 ZIP 文件
  • 创建临时文件或目录
Java API 支持许多其他任务,这些任务在Java I/O API 教程中有详细说明。
本文重点介绍 Java 8 以来的 API 改进。特别是:

1、读取文本文件
您可以像这样将文本文件读入字符串:

String content = Files.readString(path);

这里,path是 的一个实例java.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

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

Files.lines如果您可以使用流操作(例如map、 )自然地处理行,也可以使用filter。请注意,返回的流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。

 

2、写入文本文件
您只需一次调用即可将字符串写入文本文件:

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

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

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

如需更通用的输出,PrintWriter如果您想使用该printf方法,请使用:

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.NumberFormat或Integer.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完成后请记得关闭。

 

3、从输入流读取
也许使用流的最常见原因是从网站上读取一些内容。

如果需要设置请求标头或读取响应标头,请使用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 的附加信息来确定图像类型。

 

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

4、遍历目录和子目录中的条目
大多数情况下,您可以使用以下两种方法之一。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();
    . . .
}

以下是遍历目录条目的其他方法:
  • Files.walk 的重载版本可让您限制遍历树的深度。
  • 两个 Files.walkFileTree 方法可在首次和最后一次访问目录时通知 FileVisitor,从而对遍历过程进行更多控制。 这偶尔会很有用,特别是在清空和删除目录树时。 有关详情,请参阅教程 "走动文件树"。
  • Files.find 方法就像 Files.walk,只是提供了一个过滤器来检查每个路径及其 BasicFileAttributes。
  • 两个 Files.newDirectoryStream(Path) 方法产生 DirectoryStream 实例,可用于增强 for 循环。
  • 传统的 File.list 或 File.listFiles 方法会返回文件名或文件对象,与使用 Files.list 相比没有优势。 这些方法现已过时。


5、使用 ZIP 文件
自 Java 1.1 以来,ZipInputStream和ZipOutputStream类提供了用于处理 ZIP 文件的 API。但该 API 有点笨重。Java 8 引入了一个更好的ZIP 文件系统:

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

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

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

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

要读取文件内容,只需使用Files.readString或Files.readAllBytes:

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

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

6、创建临时文件和目录
我经常需要收集用户输入、生成文件并运行外部进程。然后我会使用临时文件(下次重启后会消失)或临时目录(进程完成后会删除)。

Files.createTempFile为此,我使用了这两种方法Files.createTempDirectory。

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

这将在合适的位置(/tmp在 Linux 中)创建一个临时文件或目录,并带有给定的前缀和文件后缀。

 

结论
网络搜索和 AI 聊天可能会为常见的 I/O 操作推荐不必要的复杂代码。通常有更好的替代方案:

  • 您不需要循环来读取或写入字符串或字节数组。
  • 您甚至可能不需要流、reader 或writer。
  • 熟悉Files创建、复制、移动和删除文件和目录的方法。
  • 使用Files.list或Files.walk遍历目录条目。
  • 使用 ZIP 文件系统处理 ZIP 文件。
  • 远离遗留File类。