Writing Files in Java
Write text and binary files in Java with FileWriter, BufferedWriter, PrintWriter, and Files.writeString.
Writing a file in Java means turning data in memory — a String, a List of lines, or a byte[] — into bytes on disk. This chapter covers the five writers you'll actually reach for, when each one fits, the StandardOpenOption flags that decide overwrite-vs-append behaviour, and the single most common write bug: data that "didn't save" because the writer was never closed.
Writing follows the same shape as reading from the previous chapter — modern one-liners on top of Files, classic decorators on top of FileWriter, and a small set of options that decide what happens when the target file does or doesn't exist.
Files.writeString(path, text) — whole file, one call
The peer of Files.readString. Added in Java 11.
Files.writeString(Path.of("notes.txt"), "hello world\n", StandardCharsets.UTF_8);Default open options are CREATE, WRITE, TRUNCATE_EXISTING — i.e. "create if missing, overwrite if present." That default trips people who expect append behaviour; you opt into it explicitly:
Files.writeString(path, "another line\n", StandardCharsets.UTF_8,
StandardOpenOption.CREATE, StandardOpenOption.APPEND);Returns the Path you gave it (handy for chaining). Use when: you have a small amount of text and you want one call. Same memory caveat as readString — don't build a 4 GB string in memory just to write it.
Files.write(path, lines) and Files.write(path, bytes)
Two overloads of the same Files.write:
Files.write(Path.of("hosts.txt"), List.of("alpha", "beta", "gamma"), StandardCharsets.UTF_8);
Files.write(Path.of("photo.png"), pngBytes);The Iterable<? extends CharSequence> overload writes each element on its own line with \n separators. The byte[] overload writes raw bytes — your go-to for binary data when the bytes are already in memory.
Files.newBufferedWriter(path) — the modern writer factory
The handle-based, streaming counterpart of Files.newBufferedReader.
try (BufferedWriter w = Files.newBufferedWriter(
Path.of("out.txt"), StandardCharsets.UTF_8, StandardOpenOption.CREATE)) {
w.write("first line");
w.newLine();
w.write("second line");
w.newLine();
}Use when: you're writing many small chunks (a loop over records, a streaming transformation, a log writer) and don't want to materialise the whole content as a string first. The buffer batches writes so the OS sees a handful of large syscalls instead of many tiny ones.
FileWriter and BufferedWriter — the classic stack
The legacy "build it yourself" version:
try (BufferedWriter w = new BufferedWriter(new FileWriter("out.txt", StandardCharsets.UTF_8))) {
for (String line : lines) {
w.write(line);
w.newLine();
}
}Three layers, bottom up: FileWriter writes raw characters using the charset you supply (or the platform default — never do this); BufferedWriter wraps it with an in-memory buffer and a portable newLine() method. Same shape, more keystrokes than the Files.newBufferedWriter form. New code prefers the modern factory; you'll see this stack in older code.
The second constructor argument on FileWriter is append:
new FileWriter("out.txt", true); // append mode (boolean)
new FileWriter("out.txt", StandardCharsets.UTF_8); // overwrite, UTF-8
new FileWriter("out.txt", StandardCharsets.UTF_8, true); // append, UTF-8The (String, boolean) constructor predates the charset-aware ones. Mixing the two in the same codebase is one of those legacy maintenance hazards — same class, two competing argument orders.
PrintWriter — formatted output
PrintWriter adds print, println, and printf on top of any Writer. It's the same API you've been using on System.out (which is itself a PrintStream, the byte-oriented sibling).
try (PrintWriter w = new PrintWriter(Files.newBufferedWriter(Path.of("report.txt")))) {
w.println("Report generated");
w.printf("user = %-10s total = %d%n", "alice", 42);
w.printf("user = %-10s total = %d%n", "bob", 17);
}Two things to know:
printfuses%nfor the platform line separator.\nis hard-coded LF, which is what you usually want for log files and machine-read data.PrintWriterswallowsIOException.print,println, andprintfdo not throw — they set an internal error flag you check withcheckError(). That's a deliberate choice forSystem.out(console writes shouldn't crash a CLI tool), but it's a bug magnet for file writers. If reliable error handling matters, passfalseto the appropriate constructor and useBufferedWriterfor the writing,PrintWriteronly for formatting helpers — or querycheckError()after the writes.
StandardOpenOption flags
Every modern writer accepts OpenOption... varargs that change open semantics:
| Option | Meaning |
|---|---|
CREATE | Create the file if it doesn't exist; otherwise open the existing one. |
CREATE_NEW | Create; throw FileAlreadyExistsException if the file exists. Atomic. |
TRUNCATE_EXISTING | If the file existed, clear it on open. |
APPEND | Write at the end of the file without truncating. Atomic on most OSes. |
WRITE | Open for writing. Always implied for writers. |
SYNC / DSYNC | Block each write until the OS reports it's on disk. Slow; durability for crash-safety. |
DELETE_ON_CLOSE | Delete the file when the stream closes. |
The combinations that matter:
- Overwrite (default):
CREATE, TRUNCATE_EXISTING. WhatFiles.writeStringandFiles.newBufferedWriterdefault to. - Append:
CREATE, APPEND. The log-file pattern. - Create or fail:
CREATE_NEW. The lock-file or "don't clobber" pattern.
APPEND is OS-atomic on Unix: two processes appending to the same file get interleaved blocks but no torn writes inside a single buffered chunk. That's the contract that makes it the standard logging pattern.
The "writer wrote nothing" trap
This is the bug every Java codebase encounters once:
// WRONG — the writer is never closed
BufferedWriter w = Files.newBufferedWriter(path);
w.write("important data");
return; // tail buffer is still in memory; nothing reached the diskBufferedWriter (and PrintWriter) batches writes into an in-memory chunk. Bytes don't reach the disk until either the buffer fills or close() runs. Without try-with-resources you skip the close, and your "saved" data evaporates.
// CORRECT
try (BufferedWriter w = Files.newBufferedWriter(path)) {
w.write("important data");
} // close() runs here; tail buffer is flushedIf you need data on disk before the close — say, a tail-watcher needs to see each log line — call flush() explicitly. Files.newBufferedWriter doesn't auto-flush after each write; that's the price of the buffer.
Which writer to use
| Scenario | Pick |
|---|---|
| Small string, one shot | Files.writeString |
| List of lines or byte array | Files.write |
| Streaming many lines | Files.newBufferedWriter |
Need printf formatting | PrintWriter wrapping a buffered writer |
| Legacy code only | BufferedWriter(new FileWriter(...)) |
Default to Files.writeString for "I have the text already" and Files.newBufferedWriter for "I'll build it line by line." Reach for PrintWriter only when you need printf.
A worked example: every writer side by side
The program below writes the same content three different ways — modern one-shot, line-streamed via BufferedWriter, and printf-formatted via PrintWriter — then demonstrates APPEND versus the default TRUNCATE_EXISTING, and finally the "forgot to close" failure mode. All writes target a temp file so the example runs anywhere.
What to take from the run:
Files.writeStringandFiles.write(List)are the right calls when you have all your content already. Both overwrote the file every time because their default options includeTRUNCATE_EXISTING.BufferedWriterandPrintWriterran insidetry-with-resources. That's the only thing guaranteeing the tail buffer reaches the disk — skip it and you ship a "writer wrote nothing" bug.- The APPEND/TRUNCATE sequence wrote
base, appendedappended, then truncated and wrotetruncated. The final file contained onlytruncated\n, which is the trap — the default mode of every modern writer is to overwrite, not append. You have to opt in. CREATE_NEWon an existing path threwFileAlreadyExistsException. That's the "don't clobber" semantics — useful for lock files and atomic "have I run before?" markers.- The leaky writer had a file size of 0 before
flush()ran. The bytes were in memory, not on disk; without the manualflush()(or a properclose()), they would have been lost.
What's next
The next chapter, Deleting Files in Java, closes out the "high-level file operation" chapters with the three deleters: File.delete(), Files.delete(), and Files.deleteIfExists() — and how to remove a directory tree without writing the recursion by hand.