现代 Java 中的常见 I/O 任务
此页面由 Cay Horstmann 在 UPL 下贡献介绍
本文重点介绍应用程序程序员可能遇到的任务,尤其是在 Web 应用程序中,例如
- 读取和写入文本文件
- 从 Web 读取文本、图像、JSON
- 访问目录中的文件
- 读取 ZIP 文件
- 创建临时文件或目录
Java API 支持许多其他任务,这些任务在 Java I/O API 教程 中有详细说明。
本文重点介绍自 Java 8 以来 API 的改进。特别是
- 自 Java 18 (JEP 400) 以来,UTF-8 是 I/O 的默认编码
- 在 Java 7 中首次出现的
java.nio.file.Files
类在 Java 8、11 和 12 中添加了有用的方法 java.io.InputStream
在 Java 9、11 和 12 中获得了有用的方法java.io.File
和java.io.BufferedReader
类现在已经完全过时,即使它们经常出现在网络搜索和 AI 聊天中。
读取文本文件
您可以像这样将文本文件读取到字符串中
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<String>
延迟处理
try (Stream<String> lines = Files.lines(path)) {
. . .
}
如果您可以使用流操作(例如 map
、filter
)自然地处理行,也使用 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.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
。
从输入流读取
使用流最常见的原因可能是从网站读取内容。
如果您需要设置请求标头或读取响应标头,请使用 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();
. . .
}
以下是遍历目录条目的其他方法
Files.walk
的一个重载版本允许您限制遍历树的深度。- 两个
Files.walkFileTree
方法通过在第一次和最后一次访问目录时通知FileVisitor
,提供对迭代过程的更多控制。这在某些情况下可能有用,特别是在清空和删除目录树时。有关详细信息,请参见教程 遍历文件树。除非您需要这种控制,否则请使用更简单的Files.walk
方法。 Files.find
方法与Files.walk
相似,但您提供了一个过滤器,该过滤器检查每个路径及其BasicFileAttributes
。这比分别读取每个文件的属性效率更高。- 两个
Files.newDirectoryStream(Path)
方法生成DirectoryStream
实例,这些实例可以在增强的for
循环中使用。与使用Files.list
相比,没有优势。 - 传统的
File.list
或File.listFiles
方法返回文件名或File
对象。这些现在已经过时。
使用 ZIP 文件
从 Java 1.1 开始,ZipInputStream
和 ZipOutputStream
类提供了一个用于处理 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.readString
或 Files.readAllBytes
String contents = Files.readString(fs.getPath("/LICENSE"));
您可以使用 Files.delete
删除文件。要添加或替换文件,只需使用 Files.writeString
或 Files.write
。
创建临时文件和目录
我经常需要收集用户输入,生成文件并运行外部进程。然后我使用临时文件,这些文件在下次重启后消失,或者使用一个临时目录,我在进程完成后擦除。
我使用两种方法 Files.createTempFile
和 Files.createTempDirectory
来实现这一点。
Path filePath = Files.createTempFile("myapp", ".txt");
Path dirPath = Files.createTempDirectory("myapp");
这将在合适的位置(Linux 中的 /tmp
)创建具有给定前缀的临时文件或目录,对于文件,则具有后缀。
结论
网络搜索和 AI 聊天可能会为常见的 I/O 操作建议不必要的复杂代码。通常有更好的选择
- 您不需要循环来读取或写入字符串或字节数组。
- 您甚至可能不需要流、读取器或写入器。
- 熟悉
Files
方法,用于创建、复制、移动和删除文件和目录。 - 使用
Files.list
或Files.walk
遍历目录条目。 - 使用 ZIP 文件系统来处理 ZIP 文件。
- 远离传统的
File
类。
上次更新: 2024 年 4 月 24 日
返回教程列表