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:
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.