Write Byte Array to File in Java: Files.write and Streams

Write a byte array to file in Java with Files.write, FileOutputStream and Files.copy, covering overwrite, append, parent folders and atomic writes.

Data flow for writing to a file in Java, where binary data goes from a byte array straight to the file and text first goes through getBytes with a charset

The shortest way to write a byte[] to a file in Java is Files.write(path, bytes), which creates the file if needed, replaces any old content and writes the bytes unchanged. A FileOutputStream does the same with a File object, and Files.copy() writes data from an InputStream when the content is too large for one array.

We write byte arrays to files when we save an uploaded image, store a generated PDF or ZIP file, keep a downloaded response body or write encrypted data. Bytes need no character encoding, so the file contains the same bytes as the array.

The following example writes the first four bytes of a PNG image to a file, reads them back and appends two more bytes with a FileOutputStream in append mode.

byte[] pngHeader = {(byte) 0x89, 'P', 'N', 'G'};
Path file = Files.createTempFile("avatar", ".png");

Files.write(file, pngHeader);
String written = Arrays.toString(Files.readAllBytes(file));   // [-119, 80, 78, 71]
long size = Files.size(file);                                 // 4

try (FileOutputStream out = new FileOutputStream(file.toFile(), true)) {
    out.write(new byte[] {13, 10});
}
long afterAppend = Files.size(file);                          // 6

Notice that the byte 0x89 prints as -119, because byte in Java is signed, while the file stores the same eight bits. We cover the open options of Files.write(), FileOutputStream, missing directories, large data, text bytes, atomic writes and the library helpers.

1. Writing a Byte Array With Files.write()

The method Files.write(Path, byte[], OpenOption…) opens the file, writes all bytes of the array and closes the file, even when an exception occurs. When we pass no options, it behaves as if we passed CREATE, TRUNCATE_EXISTING and WRITE, so it creates a missing file and truncates an existing file to zero bytes before writing.

Data flow for writing to a file in Java, where binary data goes from a byte array straight to the file and text first goes through getBytes with a charset
Binary data goes to the file as it is, whereas text becomes bytes only through a charset, so we pick the charset at getBytes()

We should be careful with the open options, because the default replaces an existing file without any warning. The StandardOpenOption values change that behavior, as the following sections show.

1.1. Overwriting the File

Without options, a second write replaces the first one completely, even when the new array is shorter. A user who uploads a new avatar replaces the old one this way, which is the behavior we want there.

Path file = Files.createTempFile("avatar", ".bin");

Files.write(file, new byte[] {1, 2, 3, 4, 5});
Files.write(file, new byte[] {9, 9});
String content = Arrays.toString(Files.readAllBytes(file));   // [9, 9]

1.2. Appending Bytes

To add bytes after the existing content, we pass CREATE and APPEND. The CREATE option makes the first call work when the file does not exist yet, because APPEND alone throws a NoSuchFileException.

Path file = Files.createTempDirectory("chunks").resolve("upload.part");

Files.write(file, new byte[] {1, 2}, StandardOpenOption.CREATE, StandardOpenOption.APPEND);
Files.write(file, new byte[] {3, 4}, StandardOpenOption.CREATE, StandardOpenOption.APPEND);
String joined = Arrays.toString(Files.readAllBytes(file));   // [1, 2, 3, 4]

A resumable upload works this way, with each chunk that arrives from the client appended to a .part file. The post on appending to a file covers text appends and appends from several threads.

1.3. Failing When the File Already Exists

If we want to create a new file always, we pass StandardOpenOption.CREATE_NEW. The method throws a FileAlreadyExistsException if the file exists, and the check and the creation happen as one atomic step, so two threads cannot both create the same file.

Path file = Files.createTempDirectory("invoices").resolve("invoice-1.pdf");

Files.write(file, new byte[] {37, 80, 68, 70}, StandardOpenOption.CREATE_NEW);
Path again = Files.write(file, new byte[] {0}, StandardOpenOption.CREATE_NEW);   // FileAlreadyExistsException

We use CREATE_NEW for files that must never be replaced, such as issued invoices, instead of checking Files.exists() first, because another thread can create the file between the check and the write.

2. Writing Bytes With FileOutputStream

A FileOutputStream writes bytes to a file that we name with a File object or a path string. We use it when an API gives us a File, or when we write the data in several pieces. The constructor with true as the second argument appends, and the one without it truncates the file.

Path file = Files.createTempFile("report", ".bin");
byte[] data = {10, 20, 30, 40, 50};

try (FileOutputStream out = new FileOutputStream(file.toFile())) {
    out.write(data);
    out.write(data, 1, 2);
}
String content = Arrays.toString(Files.readAllBytes(file));   // [10, 20, 30, 40, 50, 20, 30]

The call write(data, 1, 2) writes 2 bytes starting at index 1, which is handy when only part of a buffer holds valid data. The NIO equivalent is Files.newOutputStream(path), which takes the same open options as Files.write(). We close the stream with try-with-resources, so the file handle is released even when write() throws.

3. Creating Missing Parent Directories

Neither Files.write() nor FileOutputStream creates missing directories. Say a web app stores avatars under uploads/users/42/avatar.png. The first upload of a new user fails, because the directory for that user does not exist yet.

Path uploads = Files.createTempDirectory("uploads");
Path avatar = uploads.resolve("users/42/avatar.png");
byte[] image = {(byte) 0x89, 'P', 'N', 'G'};

Path failed = Files.write(avatar, image);   // NoSuchFileException

Files.createDirectories(avatar.getParent());
Files.write(avatar, image);
long size = Files.size(avatar);             // 4

The method Files.createDirectories() creates every missing directory in the path and does nothing when they already exist, so we can call it before every write. A FileOutputStream reports the same problem as a FileNotFoundException.

4. Writing Large Data Without a Big Array

A byte[] holds the whole content in memory, and an array can never have more than about 2 GB. When the data comes from a stream, such as an HTTP download or a database BLOB, we copy the stream to the file instead of collecting it into an array first.

A nightly job that downloads a 3 GB backup is a typical case. The method Files.copy(InputStream, Path, CopyOption…) reads the stream in small blocks and writes each block to the file, so memory use stays the same for any size.

Path backup = Files.createTempDirectory("backups").resolve("db.bak");
byte[] block = new byte[1000];

try (InputStream in = new ByteArrayInputStream(block)) {
    long copied = Files.copy(in, backup);
}
long size = Files.size(backup);   // 1000

try (InputStream in = new ByteArrayInputStream(block);
     OutputStream out = Files.newOutputStream(backup, StandardOpenOption.APPEND)) {
    long transferred = in.transferTo(out);
}
long total = Files.size(backup);  // 2000

Without StandardCopyOption.REPLACE_EXISTING, Files.copy() throws a FileAlreadyExistsException if the target exists. The method InputStream.transferTo() (Java 9) copies to any OutputStream, which lets us choose the open options. With the JDK HTTP client, HttpResponse.BodyHandlers.ofFile(path) writes a response body to a file without an array at all. The post on copying an InputStream to an OutputStream has more variants.

5. Turning Text Into Bytes

When the bytes come from a String, the charset decides which bytes we get. The call getBytes() without an argument uses the default charset, which is UTF-8 since Java 18 but was the operating system charset before. We pass the charset explicitly, so the file is the same on every JVM.

String word = "caf\u00e9";

byte[] utf8 = word.getBytes(StandardCharsets.UTF_8);
byte[] latin1 = word.getBytes(StandardCharsets.ISO_8859_1);
int utf8Length = utf8.length;       // 5
int latin1Length = latin1.length;   // 4

For text, Files.writeString(path, text, charset) is shorter than converting to bytes ourselves, and it throws an exception for characters the charset cannot encode, whereas getBytes() replaces them with ?. The post on reading and writing UTF-8 data explains encodings in detail.

6. Writing Atomically and Verifying the Result

If the app crashes in the middle of Files.write(), the file is left truncated or half written. For files that other code reads at any time, such as a cached thumbnail, we write the bytes to a temp file in the same directory and move it over the target in one step.

Path dir = Files.createTempDirectory("cache");
Path thumbnail = dir.resolve("thumb.png");
byte[] image = {(byte) 0x89, 'P', 'N', 'G'};

Path tmp = Files.createTempFile(dir, "thumb", ".tmp");
Files.write(tmp, image);
Files.move(tmp, thumbnail, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);
boolean same = Arrays.equals(Files.readAllBytes(thumbnail), image);   // true

To compare two files without reading both into memory, Files.mismatch(path1, path2) (Java 12) returns -1 when the contents are equal, or the position of the first different byte. That is useful in tests that check a written file against an expected one.

Path dir = Files.createTempDirectory("check");
Path expected = Files.write(dir.resolve("expected.bin"), new byte[] {1, 2, 3});
Path actual = Files.write(dir.resolve("actual.bin"), new byte[] {1, 2, 3});
Path broken = Files.write(dir.resolve("broken.bin"), new byte[] {1, 7, 3});

long equal = Files.mismatch(expected, actual);       // -1
long differs = Files.mismatch(expected, broken);     // 1

7. Commons IO and Guava Helpers

Apache Commons IO and Guava both have a one-line helper that takes a java.io.File. The Commons IO method FileUtils.writeByteArrayToFile() also creates missing parent directories, and its overload with a boolean appends. We use them in projects that already have the library, because Files.write() does the same job without a dependency.

File file = Files.createTempDirectory("lib").resolve("out/data.bin").toFile();
byte[] data = {1, 2, 3};

FileUtils.writeByteArrayToFile(file, data);
FileUtils.writeByteArrayToFile(file, data, true);
long commonsSize = file.length();                    // 6

com.google.common.io.Files.write(data, file);
long guavaSize = file.length();                      // 3

The Guava call uses the fully qualified name com.google.common.io.Files, since java.nio.file.Files is already imported under the same simple name. The Guava method overwrites the file and needs the parent directory to exist.

8. Byte Array to File FAQs

Most follow-up questions come from code that receives bytes from an upload, a database or an HTTP response and has to put them on disk.

8.1. How Do I Convert a byte[] to a File Object?

A java.io.File holds only a path, not content, so there is nothing to convert. We write the bytes to a path with Files.write() and pass path.toFile() to the API that expects a File. When the file is only needed for a moment, the post on creating a temporary file shows how to create and clean up a temp file.

8.2. Does Files.write() Overwrite an Existing File?

Yes. Without open options, it truncates the existing file and writes the new bytes. We pass CREATE and APPEND to keep the old content, or CREATE_NEW to get an exception instead of replacing the file, as shown in section 1.

8.3. How Do I Write Only Part of a Byte Array?

We call write(bytes, offset, length) on an OutputStream, because Files.write() always writes the whole array. Another option is Arrays.copyOfRange(), which copies the part we need into a new array.

8.4. How Do I Write a ByteBuffer to a File?

We open a FileChannel and call write(buffer) in a loop until hasRemaining() returns false, because one call can write fewer bytes than the buffer holds.

Path file = Files.createTempFile("buffer", ".bin");
ByteBuffer buffer = ByteBuffer.wrap(new byte[] {1, 2, 3, 4});

try (FileChannel channel = FileChannel.open(file, StandardOpenOption.WRITE)) {
    while (buffer.hasRemaining()) {
        channel.write(buffer);
    }
}
long size = Files.size(file);   // 4

The post on NIO buffers explains position, limit and flip().

8.5. Do I Need a BufferedOutputStream to Write a Byte Array?

No. A single write(byte[]) call passes the whole array to the operating system at once, so a buffer adds nothing. A BufferedOutputStream helps when the code writes many small pieces, such as one byte at a time.

9. Conclusion

A single Files.write(path, bytes) call writes a byte array to a file in Java. It creates and truncates the file by default, CREATE plus APPEND adds to the end, and CREATE_NEW refuses to replace an existing file.

A FileOutputStream fits APIs that hand us a File or data in pieces, and Files.copy() or transferTo() write large data from a stream without a big array. We create parent directories first, pass a charset when the bytes come from text, and use a temp file with an atomic move for files that must never be half written. The guide on writing to a file in Java compares all write methods, and the Java IO guide lists the related tasks, including reading a file into a byte array.

10. References

Happy Learning !!

Source Code on Github

About Us

HowToDoInJava provides tutorials and how-to guides on Java and related technologies.

It also shares the best practices, algorithms & solutions and frequently asked interview questions.