1. 概述

文件处理是我们经常遇到的基本操作之一。在将数据写入文件时,通常会使用FileWriter类。这个类中,有两个重要方法——flush()close(),它们在管理文件输出流时各司其职。

本教程将介绍FileWriter的常用用法,并深入探讨flush()close()方法的区别。

2. FileWritertry-with-resources

Java中的try-with-resources语句是管理资源的强大工具,特别是在处理文件操作,如文件处理时。它会在代码块退出(正常或异常)时自动关闭声明中的资源。

然而,有些情况下,直接使用FileWritertry-with-resources可能不理想或没必要。FileWriter在有无try-with-resources的情况下行为可能会有所不同。接下来,我们将深入了解这一点。

2.1. 使用FileWritertry-with-resources

当我们在try-with-resources块中使用FileWriter时,文件输出流对象会自动在退出try-with-resources块时被刷新并关闭。

下面是一个单元测试示例:

@Test
void whenUsingFileWriterWithTryWithResources_thenAutoClosed(@TempDir Path tmpDir) throws IOException {
    Path filePath = tmpDir.resolve("auto-close.txt");
    File file = filePath.toFile();

    try (FileWriter fw = new FileWriter(file)) {
        fw.write("Catch Me If You Can");
    }

    List<String> lines = Files.readAllLines(filePath);
    assertEquals(List.of("Catch Me If You Can"), lines);
}

由于测试需要读写文件,我们使用了JUnit5的临时目录扩展@TempDir)。这个扩展允许我们在测试中专注于核心逻辑,无需手动创建和管理测试用的临时目录和文件。

正如测试方法所示,我们在try-with-resources块中写入字符串。然后,当我们使用[Files.readAllLines()](/reading-file-in-java#1-reading-a-small-file)检查文件内容时,会得到预期的结果。

2.2. 不使用try-with-resourcesFileWriter

然而,如果不使用try-with-resourcesFileWriter对象不会自动被刷新和关闭:

@Test
void whenUsingFileWriterWithoutFlush_thenDataWontBeWritten(@TempDir Path tmpDir) throws IOException {
    Path filePath = tmpDir.resolve("noFlush.txt");
    File file = filePath.toFile();
    FileWriter fw = new FileWriter(file);
    fw.write("Catch Me If You Can");

    List<String> lines = Files.readAllLines(filePath);
    assertEquals(0, lines.size());
    fw.close(); //close the resource
}

如上述测试所示,尽管我们通过FileWriter.write()向文件写入了一些文本,但文件仍然为空。

现在,让我们解决这个问题。

3. FileWriter.flush()FileWriter.close()

本节首先解决“文件仍然为空”的问题,然后讨论flush()close()方法之间的差异。

3.1. 解决“文件仍为空”问题

首先,快速理解为什么调用FileWriter.write()后文件仍为空。当我们调用FileWriter.write()时,数据并不会立即写入磁盘上的文件,而是暂存在缓冲区中。因此,为了在文件中看到数据,我们需要将缓冲区的数据刷新到文件中

一个简单的方法是调用flush()方法:

@Test
void whenUsingFileWriterWithFlush_thenGetExpectedResult(@TempDir Path tmpDir) throws IOException {
    Path filePath = tmpDir.resolve("flush1.txt");
    File file = filePath.toFile();
    
    FileWriter fw = new FileWriter(file);
    fw.write("Catch Me If You Can");
    fw.flush();

    List<String> lines = Files.readAllLines(filePath);
    assertEquals(List.of("Catch Me If You Can"), lines);
    fw.close(); //close the resource
}

如图所示,调用flush()后,我们可以读取文件并获取预期的数据。

另一种方法是调用close()方法,将缓冲区的数据转移到文件。这是因为close()方法首先执行flush,然后关闭文件流。

接下来,我们创建一个测试来验证这一点:

@Test
void whenUsingFileWriterWithClose_thenGetExpectedResult(@TempDir Path tmpDir) throws IOException {
    Path filePath = tmpDir.resolve("close1.txt");
    File file = filePath.toFile();
    FileWriter fw = new FileWriter(file);
    fw.write("Catch Me If You Can");
    fw.close();

    List<String> lines = Files.readAllLines(filePath);
    assertEquals(List.of("Catch Me If You Can"), lines);
}

这看起来与flush()调用类似。但是,在处理文件输出流时,这两个方法有不同的作用。

3.2. flush()close()方法的区别

flush()方法主要用于强制立即写出缓冲区中的任何数据,而不会关闭FileWriter,以便继续对文件进行写入或追加操作。相反,close()方法则既执行刷新操作又释放关联的资源

换句话说,调用flush()确保缓冲区中的数据及时写入磁盘,即使不关闭流也能继续写入文件。而当调用close()时,它会将现有缓冲区的数据写入文件并关闭它。因此,除非重新打开流,例如初始化一个新的FileWriter对象,否则无法再向文件写入数据。

下面通过一些例子来理解:

@Test
void whenUsingFileWriterWithFlushMultiTimes_thenGetExpectedResult(@TempDir Path tmpDir) throws IOException {
    List<String> lines = List.of("Catch Me If You Can", "A Man Called Otto", "Saving Private Ryan");
    Path filePath = tmpDir.resolve("flush2.txt");
    File file = filePath.toFile();
    FileWriter fw = new FileWriter(file);
    for (String line : lines) {
        fw.write(line + System.lineSeparator());
        fw.flush();
    }

    List<String> linesInFile = Files.readAllLines(filePath);
    assertEquals(lines, linesInFile);
    fw.close(); //close the resource
}

在这个例子中,我们三次调用write()分别写入三行到文件。每次write()调用后,我们都调用了flush()。最后,我们可以从目标文件中读取出三行。

然而,如果在调用FileWriter.close()后尝试写入数据,将会抛出IOException,错误信息为“Stream closed”:

@Test
void whenUsingFileWriterWithCloseMultiTimes_thenGetIOExpectedException(@TempDir Path tmpDir) throws IOException {
    List<String> lines = List.of("Catch Me If You Can", "A Man Called Otto", "Saving Private Ryan");
    Path filePath = tmpDir.resolve("close2.txt");
    File file = filePath.toFile();
    FileWriter fw = new FileWriter(file);
    //write and close
    fw.write(lines.get(0) + System.lineSeparator());
    fw.close();

    //writing again throws IOException
    Throwable throwable = assertThrows(IOException.class, () -> fw.write(lines.get(1)));
    assertEquals("Stream closed", throwable.getMessage());
}

4. 总结

在这篇文章中,我们探讨了FileWriter的常用用法,并讨论了flush()close()方法的区别。如往常一样,所有示例的完整源代码可在GitHub上找到:https://github.com/eugenp/tutorials/tree/master/core-java-modules/core-java-io-apis-2