1. Overview

In this tutorial, we’ll look at the performance differences between two basic Java classes for writing files: FileWriter and BufferedWriter. While conventional wisdom on the web often suggests that BufferedWriter typically outperforms FileWriter, our goal is to put this assumption to the test.

After looking at the basic information about using classes, their inheritance, and their internal implementation, we’ll use the Java Microbenchmark Harness (JMH) to test whether BufferedWriter really has an advantage.

We’ll run the tests with JDK17 on Linux, but we can expect similar results with any recent version of the JDK on any operating system.

2. Basic Usage

FileWriter writes text to character files using a default buffer whose size isn’t specified in the Javadoc:

FileWriter writer = new FileWriter("testFile.txt");
writer.write("Hello, Baeldung!");
writer.close();

BufferedWriter is an alternative choice. It’s designed to wrap around other Writer classes, including FileWriter:

int BUFSIZE = 4194304; // 4MiB
BufferedWriter writer = new BufferedWriter(new FileWriter("testBufferedFile.txt"), BUFSIZE);
writer.write("Hello, Buffered Baeldung!");
writer.close();

In this case, we specified a 4MiB buffer. However, if we don’t set the size for the buffer, its default size isn’t specified in the Javadoc.

3. Inheritance

Here is a UML diagram illustrating the inheritance structure of FileWriter and BufferedWriter:

FileWrite and BufferedWriter Hierarchy

It’s helpful to understand that FileWriter* and BufferedWriter both extend Writer, and the operation of FileWriter is based on *OutputStreamWriter. Unfortunately, neither the analysis of inheritance hierarchies nor the Javadocs tell us enough about the default buffer size of FileWriter and BufferedWriter, so we’ll inspect the JDK source code to understand more.

4. Underlying Implementation

Looking at the underlying implementation of FileWriter, we see that its default buffer size is 8192 bytes from JDK10 to JDK18, and variable from 512 to 8192 in later versions. Specifically, FileWriter extends OutputStreamWriter, as we’ve just seen in the UML diagram, and OutputStreamWriter uses StreamEncoder, whose code contains DEFAULT_BYTE_BUFFER_SIZE = 8192 up to JDK18 and MAX_BYTE_BUFFER_CAPACITY = 8192 in later versions.

StreamEncoder isn’t a public class in the JDK API. It’s an internal class in the sun.nio.cs package that is used within the Java framework to handle encoding of character streams.

Its buffer size allows FileWriter to handle data efficiently by minimizing the number of I/O operations. Since the default character encoding in Java is typically UTF-8, 8192 bytes would correspond to approximately 8192 characters in most scenarios. Despite this efficient buffering, FileWriter is still considered to have no buffering capabilities due to outdated documentation.

The default buffer size of BufferedWriter is the same as FileWriter. We can verify it by checking its source code, which contains defaultCharBufferSize = 8192 from JDK10 to JDK18, and DEFAULT_MAX_BUFFER_SIZE = 8192 in later versions. However, BufferedWriter allows us to specify a different buffer size, as we’ve seen in the previous example.

5. Comparing Performance

Here we’ll compare FileWriter and BufferedWriter with JMH. If we want to replicate the tests on our machine, and if we’re using Maven, we’ll need to set up the JMH dependencies on pom.xml, add the JMH annotation processor to the Maven compiler plugin configuration, and make sure all the required classes and resources are available during execution. The Getting Started section of our JMH tutorial covers these points.

5.1. Disk Write Synchronization

To perform disk write benchmarking with JHM, it’s imperative to achieve full synchronization of disk operations by disabling the operating system cache. This step is critical because asynchronous disk writes can significantly affect the accuracy of I/O operations measurements. By default, operating systems store frequently accessed data in memory, reducing the number of actual disk writes and invalidating benchmark results.

On Linux systems, we can remount the filesystem with the sync option of mount to disable caching and ensure that all write operations are immediately synchronized to disk:

$ sudo mount -o remount,sync /path/to/mount

Similarly, the macOS mount has a sync option that ensures that all I/O to the filesystem is synchronous.

On Windows, we open the Device Manager and expand the Drives section. Then we right-click on the drive we want to configure, select Properties, and navigate to the Policies tab. Finally, we disable the Enable write caching on the device option.

5.2. Our Tests

Our code measures the performance of FileWriter and BufferedWriter under various write conditions. We run several benchmarks to test single writes and repeated writes (10, 1000, 10000, and 100000 times) to the benchmark.txt file.

We use JMH-specific annotations to configure the benchmark parameters such as @Benchmark, @State, @BenchmarkMode, and others to set the scope, mode, warm-up iterations, measurement iterations, and fork settings.

The main method sets up the environment by deleting any existing benchmark.txt file and adjusting the classpath before running the JMH benchmarking suite:

@State(Scope.Benchmark)
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
@Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
@Measurement(iterations = 10, time = 10, timeUnit = TimeUnit.SECONDS)
@Fork(1)
public class BenchmarkWriters {
    private static final Logger log = LoggerFactory.getLogger(BenchmarkWriters.class);
    private static final String FILE_PATH = "benchmark.txt";
    private static final String CONTENT = "This is a test line.";
    private static final int BUFSIZE = 4194304; // 4MiB

    @Benchmark
    public void fileWriter1Write() {
        try (FileWriter writer = new FileWriter(FILE_PATH, true)) {
            writer.write(CONTENT);
            writer.close();
        } catch (IOException e) {
            log.error("Error in FileWriter 1 write", e);
        }
    }

    @Benchmark
    public void bufferedWriter1Write() {
        try (BufferedWriter writer = new BufferedWriter(new FileWriter(FILE_PATH, true), BUFSIZE)) {
            writer.write(CONTENT);
            writer.close();
        } catch (IOException e) {
            log.error("Error in BufferedWriter 1 write", e);
        }
    }

    @Benchmark
    public void fileWriter10Writes() {
        try (FileWriter writer = new FileWriter(FILE_PATH, true)) {
            for (int i = 0; i < 10; i++) {
                writer.write(CONTENT);
            }
            writer.close();
        } catch (IOException e) {
            log.error("Error in FileWriter 10 writes", e);
        }
    }

    @Benchmark
    public void bufferedWriter10Writes() {
        try (BufferedWriter writer = new BufferedWriter(new FileWriter(FILE_PATH, true), BUFSIZE)) {
            for (int i = 0; i < 10; i++) {
                writer.write(CONTENT);
            }
            writer.close();
        } catch (IOException e) {
            log.error("Error in BufferedWriter 10 writes", e);
        }
    }

    @Benchmark
    public void fileWriter1000Writes() {
        [...]
    }

    @Benchmark
    public void bufferedWriter1000Writes() {
        [...]
    }
    
    @Benchmark
    public void fileWriter10000Writes() {
        [...]
    }

    @Benchmark
    public void bufferedWriter10000Writes() {
        [...]
    }
    
    @Benchmark
    public void fileWriter100000Writes() {
        [...]
    }

    @Benchmark
    public void bufferedWriter100000Writes() {
        [...]
    }

    [...]
}

In these tests, each benchmark method opens and closes the file writer independently. The @Fork(1) annotation indicates that only one fork is used, so there are no multiple parallel executions of the same benchmark method. The code doesn’t explicitly create or manage threads, so all writes are done in the main thread of the benchmark.

All this means that the writes are indeed sequential and not concurrent, which is necessary to get valid measurements.

5.3. Results

These are the results with the BufferedWriter buffer size of 4MiB specified in the code:

Benchmark                                    Mode  Cnt     Score     Error  Units
BenchmarkWriters.bufferedWriter100000Writes  avgt   10  9170.583 ± 245.916  ms/op
BenchmarkWriters.bufferedWriter10000Writes   avgt   10   918.662 ±  15.105  ms/op
BenchmarkWriters.bufferedWriter1000Writes    avgt   10   114.261 ±   2.966  ms/op
BenchmarkWriters.bufferedWriter10Writes      avgt   10    37.999 ±   1.571  ms/op
BenchmarkWriters.bufferedWriter1Write        avgt   10    37.968 ±   2.219  ms/op
BenchmarkWriters.fileWriter100000Writes      avgt   10  9253.935 ± 261.032  ms/op
BenchmarkWriters.fileWriter10000Writes       avgt   10   951.684 ±  41.391  ms/op
BenchmarkWriters.fileWriter1000Writes        avgt   10   114.610 ±   4.366  ms/op
BenchmarkWriters.fileWriter10Writes          avgt   10    37.761 ±   1.836  ms/op
BenchmarkWriters.fileWriter1Write            avgt   10    37.912 ±   2.080  ms/op

Instead, these are the results without specifying a buffer value for BufferedWriter, i.e., using its default buffer:

Benchmark                                    Mode  Cnt     Score     Error  Units
BenchmarkWriters.bufferedWriter100000Writes  avgt   10  9117.021 ± 143.096  ms/op
BenchmarkWriters.bufferedWriter10000Writes   avgt   10   931.994 ±  34.986  ms/op
BenchmarkWriters.bufferedWriter1000Writes    avgt   10   113.186 ±   2.076  ms/op
BenchmarkWriters.bufferedWriter10Writes      avgt   10    40.038 ±   2.042  ms/op
BenchmarkWriters.bufferedWriter1Write        avgt   10    38.891 ±   0.684  ms/op
BenchmarkWriters.fileWriter100000Writes      avgt   10  9261.613 ± 305.692  ms/op
BenchmarkWriters.fileWriter10000Writes       avgt   10   932.001 ±  26.676  ms/op
BenchmarkWriters.fileWriter1000Writes        avgt   10   114.209 ±   5.988  ms/op
BenchmarkWriters.fileWriter10Writes          avgt   10    38.205 ±   1.361  ms/op
BenchmarkWriters.fileWriter1Write            avgt   10    37.490 ±   2.137  ms/op

In essence, these results show that the performance of FileWriter and BufferedWriter is nearly identical under all test conditions. Furthermore, specifying a larger buffer for BufferedWriter than the default one doesn’t provide any benefit.

6. Conclusion

In this article, we explored the performance differences between FileWriter and BufferedWriter using JHM. We began by looking at their basic usage and inheritance structures. Both classes have a default buffer size of 8192 bytes from JDK10 to JDK18, and variable from 512 to 8192 bytes in later versions.

We ran benchmarks to compare their performance under various conditions, ensuring accurate measurements by disabling operating system caching. The tests included single and repetitive writes using both the default and a specified 4MiB buffer for BufferedWriter.

Our results show that FileWriter and BufferedWriter have nearly identical performance in all scenarios. Furthermore, increasing the buffer size of BufferedWriter doesn’t significantly improve performance.

As always, the full source code is available over on GitHub.