Java files - Basics: writing files - Feature image

Java files, part 2: How to write files quickly and easily

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 FileWriterFileOutputStreamOutputStreamReaderBufferedOutputStream and BufferedWriter?
  • When to use Files.newOutputStream() and Files.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);

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);

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);

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);

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 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);
    }
}

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);
    }
}

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:

FileOutputStream – write speed in relation to buffer size
FileOutputStream – write speed in relation to 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);
    }
}

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);
    }
}

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);
    }
}

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);
    }
}

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);
    }
}

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);
    }
}

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);
    }
}

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:

Comparison of the times needed to write 100 million bytes / characters to a file
Comparison of the times needed to write 100 million bytes/characters to a 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:

Comparison of the times needed to write 100 million bytes / characters to a file (buffered)
Comparison of the times needed to write 100 million bytes/characters to a file (buffered)

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 and OutputStreamWriter classes internally use StreamEncoder.forOutputStreamWriter(). If you call this method without a specific character encoding, it uses Charset.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() and Files.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 11
  • new 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 future articles of this series, I will show:

  • How to use the File, Path, and Paths classes to construct file and directory paths.
  • 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, DataInputStream, and Scanner to write and read structured data?

In the further course of the series, more advanced topics will be covered:

  • The NIO.2 channels and buffers introduced in Java 7 to speed up working with large files in particular.
  • File locking to access the same files from multiple threads or processes in parallel without conflict.
  • Memory-mapped I/O for ultra-fast file access without streams.

Would you like to be informed when future articles are published? Then you can sign up for my newsletter by filling out the following form. If you liked the article, I’m also happy if you share it via one of the buttons at the end.

Leave a Comment

Your email address will not be published. Required fields are marked *