Writing Files in Java
Write text and binary files in Java with FileWriter, BufferedWriter, PrintWriter, and Files.writeString.
Writing Files in Java
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. The chapter walks five writers, then the four StandardOpenOption flags you'll actually use, then the trap that catches one beginner per team every quarter: a writer that "didn't write anything" because it was never closed.
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.
Practice
`Files.writeString(path, text)` with no `OpenOption` arguments. What does it do if the file already exists?