After the first part of the series was about reading files in Java, this second part introduces the corresponding methods for writing small and large files.
The article addresses the following questions in detail:
- What is the easiest way to write a string or a list of strings to a text file?
- How to write a byte array to a binary file?
- When processing large amounts of data, how do you write the data directly to files (without first having to build up the complete contents of the file in memory)?
- When to use
FileWriter
,FileOutputStream
,OutputStreamReader
,BufferedOutputStream
andBufferedWriter
? - When to use
Files.newOutputStream()
andFiles.newBufferedWriter()
?
I already mentioned the topic of operating system independence in the first part, i.e., what to consider when coding characters, line breaks, and path names.
What is the easiest way to write to a file in Java?
Up to and including Java 6, there was no easy way to write files. You had to open a FileOutputStream
or a FileWriter
, if necessary, wrap it with a BufferedOutputStream
or BufferedWriter
, write into it, and finally – also in case of an error – close all streams again.
In Java 7, the utility class java.nio.file.Files
was added with the "NIO.2 File API" (NIO stands for New I/O). This class contains methods to write a byte array, a String, or a list of Strings to a file with a single command.
Writing a byte array to a binary file
You can write a byte array to a file with the following command:
String fileName = ...;
byte[] bytes = ...;
Files.write(Path.of(fileName), bytes);
Code language: Java (java)
The method expects a Path
object as the first parameter. It describes a file or directory name and provides utility methods for constructing it. In the example, the static Path.of()
method – available since Java 11 – is used to create a Path
object from a file name. Before Java 11, you can use Paths.get()
instead. Internally, both methods call FileSystems.getDefault().getPath()
.
Writing a String to a text file
It is just as easy to write a string to a file – but only since Java 11:
String fileName = ...;
String text = ...;
Files.writeString(Path.of(fileName), text);
Code language: Java (java)
Writing a list of Strings to a text file
Often you don't write a single String into a text file, but several Strings as lines. With the following command, you can write a String list (or more precisely: an Iterable<? extends CharSequence>
) into a text file:
String fileName = ...;
List<String> lines = ...;
Files.write(Path.of(fileName), lines);
Code language: Java (java)
Writing a String Stream to a text file
There is no one-to-one equivalent of the Stream-generating method Files.lines()
, that is, no method that writes directly from a String Stream to a file. However, with a small workaround, it is still possible:
String fileName = ...;
Stream<String> lines = ...;
Files.write(Path.of(fileName), (Iterable<String>) lines::iterator);
Code language: Java (java)
What are we doing here? The Files.write()
method used here is the same as in the previous example, that is, the one that accepts an Iterable<String>
. A Java 8 Stream itself is not an Iterable
since you cannot iterate over it multiple times, but only once. Therefore you cannot pass the Stream itself as a parameter.
However, Iterable
is a functional interface whose only method iterator()
returns an Iterator
. Therefore we can pass a method reference to lines.iterator()
(which also returns an iterator) as Iterable
.
You can this because you can assume that Files.write()
calls the referenced iterator()
method only once. If the iterator()
method were called a second time, the Stream would acknowledge this with an IllegalStateException
with the message "stream has already been operated upon or closed."
Writing files with java.nio.file.Files – Summary
In this chapter, you got to know the utility methods of the java.nio.file.Files
class. These methods are suitable for all use cases where the data you want to write to a file is completely stored in memory.
However, if the data is generated incrementally, you should also write it to a file incrementally. You should not first "collect" all the bytes in memory and then write them to a file in one go using one of the methods shown above. Only if the amount of data is only a few kilobytes, this is okay.
In such a case, you'd better work (directly or indirectly) with a FileOutputStream
. The following chapter explains how to do that.
How to write data to a file without having to collect its entire content in memory first?
To progressively write data to a file, use a FileOutputStream
(or related classes). These were available before Java 7 and made writing small files unnecessarily complicated. In the following sections, I present various options.
Writing individual bytes with FileOutputStream
The primary class is FileOutputStream
. It writes data byte by byte into a file. The following example shows how bytes returned by the imaginary process()
method are successively written to a file (until the method returns -1):
String fileName = ...;
try (FileOutputStream out = new FileOutputStream(fileName)) {
int b;
while ((b = process()) != -1) {
out.write(b);
}
}
Code language: GLSL (glsl)
Writing individual bytes is an expensive operation. Writing 100,000,000 bytes to a test file takes about 230 seconds on my system; that's just a little more than 0.4 MB per second.
Writing byte arrays with FileOutputStream
Using FileOutputStream
, you can also write byte arrays. In the following example, the process()
method returns byte arrays instead of individual bytes (and null
if no more data is available):
String fileName = ...;
try (FileOutputStream out = new FileOutputStream(fileName)) {
byte[] bytes;
while ((bytes = process()) != null) {
out.write(bytes);
}
}
Code language: GML (gml)
This method is several times faster. If you write 10 bytes 10,000,000 times (the same amount in total), you only need 24 seconds, a little more than a tenth of the previous time. If you write 100 bytes 1,000,000 times, it's only 2.6 seconds, which is a little more than a hundredth of the previous time.
What is relevant here is primarily the number of write operations, not the actual amount of data. This is because the data is written block-wise to the storage medium. Naturally, this only applies up to a specific buffer size. Writing ten gigabytes at a time is no faster than writing ten times one gigabyte. The optimal value for the buffer size depends on the hardware as well as the formatting of the medium. Using a small test program, I measured the write speed in relation to the buffer size:
On my system, the optimal buffer size is 8 KB. At this size, the write speed reaches 1,050 MB per second. 8 KB is also the optimal size on most other systems, which is why Java uses this value as default, as you can see in a later section.
Writing binary data with the NIO.2 OutputStream
In Java 7, a new method to create an OutputStream
, Files.newOutputStream()
was added:
String fileName = ...;
try (OutputStream out = Files.newOutputStream(Path.of(fileName))) {
int b;
while ((b = process()) != -1) {
out.write(b);
}
}
Code language: Java (java)
This method returns a ChannelOutputStream
instead of a FileOutputStream
. On my system, there is no relevant speed difference compared to new FileOutputStream()
when writing individual bytes or byte blocks.
Write faster with BufferedOutputStream
We have previously observed that writing blocks is much faster than writing individual bytes. BufferedOutputStream
takes advantage of this fact by first buffering the bytes to be written in a buffer and then writing them to disk when the buffer is full. By default, this buffer is 8 KB in size, which is precisely the size that leads to optimal write speed.
String fileName = ...;
try (FileOutputStream out = new FileOutputStream(fileName);
BufferedOutputStream bout = new BufferedOutputStream(out)) {
int b;
while ((b = process()) != -1) {
bout.write(b);
}
}
Code language: GLSL (glsl)
Using BufferedOutputStream
, my system needs about 250 ms to write 100,000,000 individual bytes. That's about 400 MB per second. The reason we're not reaching the 1,050 MB/s from the previous test is the overhead of the buffering logic.
Writing byte arrays with BufferedOutputStream
Just like FileOutputStream
, BufferedOutputStream
can write not only individual bytes but also byte blocks:
String fileName = ...;
try (FileOutputStream out = new FileOutputStream(fileName);
BufferedOutputStream bout = new BufferedOutputStream(out)) {
byte[] bytes;
while ((bytes = process()) != null) {
bout.write(bytes);
}
}
Code language: Java (java)
This method combines the advantages of writing byte arrays with those of a buffer. It almost always delivers optimum write speeds. For writing binary data, I always recommend using this method.
Writing text files with FileWriter
For writing text to a file, it must be converted to binary data. Character-to-byte conversion is the OutputStreamWriter
's responsibility. You wrap it around the FileOutputStream
as follows. The process()
method in the following example produces individual characters.
String fileName = ...;
try (FileOutputStream out = new FileOutputStream(fileName);
OutputStreamWriter writer = new OutputStreamWriter(out)) {
int c;
while ((c = process()) != -1) {
writer.write(c);
}
}
Code language: Java (java)
FileWriter
is more convenient. It combines FileOutputStream
and OutputStreamWriter
. The following code is equivalent to the previous one:
String fileName = ...;
try (FileWriter writer = new FileWriter(fileName)) {
int c;
while ((c = process()) != -1) {
writer.write(c);
}
}
Code language: Java (java)
OutputStreamWriter
also uses an 8 KB buffer internally. Writing 100,000,000 characters to a text file takes about 2.5 seconds.
Write text files faster with BufferedWriter
You can accelerate writing even further with BufferedWriter
:
String fileName = ...;
try (FileWriter writer = new FileWriter(fileName);
BufferedWriter bufferedWriter = new BufferedWriter(writer)) {
int c;
while ((c = process()) != -1) {
bufferedWriter.write(c);
}
}
Code language: Java (java)
BufferedWriter
adds another 8 KB buffer for characters, which are then encoded in one go when the buffer is written (instead of character by character). This second buffer reduces the writing time for 100,000,000 characters to approximately 370 ms.
Write text files faster with the NIO.2 BufferedWriter
In Java 7, the method Files.newBufferedWriter()
was added to create a BufferedWriter
:
String fileName = ...;
try (BufferedWriter bufferedWriter = Files.newBufferedWriter(Path.of(fileName))) {
int c;
while ((c = process()) != -1) {
bufferedWriter.write(c);
}
}
Code language: Java (java)
The write speed on my system is about the same as the speed of the "classically" created BufferedWriter
.
Performance overview: writing files
In the following diagram, you can see how much time the methods presented need to write 100,000,000 bytes or characters into a binary or text file:
Due to the large gap between unbuffered and buffered writing, the buffered methods hardly stand out here. The following diagram, therefore, only shows the methods that use a buffer:
Overview FileOutputStream, FileWriter, OutputStreamWriter, BufferedOutputStream, BufferedWriter
The following diagram shows the context of the java.io
classes presented in this article for writing binary and text files:
Solid lines represent binary data; dashed lines represent text data. FileWriter
combines FileOutputStream
and OutputStreamWriter
.
Character encoding
The subject of character encoding and the problems that go with it have been discussed in detail in the previous article.
Therefore, I've limited the following sections to those aspects that are relevant for writing text files with Java.
What character encoding does Java use by default to write text files?
If you do not specify a character encoding when writing a text file, Java uses a standard encoding. But be careful: Which one that is depends on the method and the Java version used.
- The
FileWriter
andOutputStreamWriter
classes internally useStreamEncoder.forOutputStreamWriter()
. If you call this method without a specific character encoding, it usesCharset.defaultCharset()
. This method, in turn, reads the character encoding from the system property "file.encoding". If the system property is not specified, it uses ISO-8859-1 by default up to Java 5 and UTF-8 since Java 6. - The
Files.writeString()
,Files.write()
andFiles.newBufferedWriter()
methods all use UTF-8 as default encoding without reading the system property mentioned above.
Due to these inconsistencies, you should always specify the character encoding. I always recommend using UTF-8. According to Wikipedia, UTF-8 encoding is used on 94.4% of all websites and can, therefore, be regarded as a de-facto standard. An exception is, of course, when you have to work with old files written in a different encoding.
How to specify the character encoding when writing a text file?
In the following you will find an example for all methods discussed so far with the character encoding set to UTF-8:
Files.writeString(path, string, StandardCharsets.UTF_8)
Files.write(path, lines, StandardCharsets.UTF_8)
new FileWriter(file, StandardCharsets.UTF_8)
// this method only exists since Java 11new InputStreamWriter(outputStream, StandardCharsets.UTF_8)
Files.newBufferedWriter(path, StandardCharsets.UTF_8)
Summary and outlook
This article has shown different methods for writing byte arrays and Strings in binary and text files in Java.
In the third part of the series, you will learn how to use the classes File
, Path
and Paths
to construct file and directory paths.
In future articles of this series, I will show:
- How to read entire directories
- How to copy, move and delete files, and how to set links
- How to create temporary files
- How to use
DataOutputStream
andDataInputStream
to write and read structured data
In the further course of the series, more advanced topics will be covered:
- The NIO channels and buffers introduced in Java 1.4 to speed up working with large files in particular.
- Memory-mapped I/O for ultra-fast file access without streams.
- File locking to access the same files from multiple threads or processes in parallel without conflict.
Would you like to be informed when future articles are published? Then click here to sign up for the HappyCoders newsletter. If you liked the article, I'm also happy if you share it via one of the buttons at the end.