Writing Files
Writing Files
Reading a file is only half the story. Knowing how to write one — and picking the right API for the job — is just as important. Java gives you several tools: the modern Files.write / Files.writeString methods from NIO.2, and the classic BufferedWriter built on the older I/O stack. Each has a distinct sweet spot, and each lets you choose between overwriting an existing file and appending to it.
The Quick Path: Files.write and Files.writeString
Since Java 7 (NIO.2), java.nio.file.Files has provided static helpers that open, write, and close the file in a single call. You do not need to manage streams or writers yourself.
Writing a list of lines:
Each element in the list becomes one line; the method appends the platform line separator after each. The file is created if it does not exist and the parent directory is present.
Writing a raw byte array: Files.write is overloaded to accept byte[] too, which is useful for binary data or when you already have the bytes:
Writing a whole string at once (Java 11+):
Files.writeString was added in Java 11 as a convenience for the very common case of writing a single String. It defaults to UTF-8 and is the shortest path from a string value to a file on disk.
Files.write and Files.writeString accept an optional Charset argument. Omitting it means UTF-8 (the default for NIO.2 methods), which is safe almost everywhere — but being explicit documents your intent and prevents surprises on rare systems with a different platform default.
Append vs Overwrite: StandardOpenOption
By default, Files.write and Files.writeString overwrite the file completely. Pass a StandardOpenOption to change that:
The key options you will use most often:
WRITE— open for writing (implicit when calling write methods).CREATE— create if absent, open if present (default behaviour).CREATE_NEW— create only; throwFileAlreadyExistsExceptionif present. Useful for safe file generation where overwriting would be a bug.APPEND— move the write position to the end before each write. Combine withCREATEto build up a log file safely.TRUNCATE_EXISTING— truncate to zero bytes on open (the default overwrite behaviour comes from this).
APPEND simultaneously, individual write calls are atomic at the OS level on most platforms, but the order of entries is unpredictable. For multi-threaded logging, prefer a dedicated logging framework or a synchronized wrapper.
BufferedWriter: When You Need Incremental Writes
Files.write is ideal when you have all the data up front — it materialises the whole content in memory before writing. When you need to build a file incrementally (writing line by line inside a loop, for example), BufferedWriter is the right tool. It accumulates writes in an in-memory buffer (default 8 KB) and flushes to disk in large, efficient chunks rather than making a system call for every single line.
A few things to note:
Files.newBufferedWriteris the NIO.2 factory — prefer it overnew BufferedWriter(new FileWriter(...))because it accepts aPathand aCharsetdirectly.writer.newLine()writes the platform's line separator (\r\non Windows,\non Unix). Avoid hardcoding"\n"in files that will be read on multiple platforms.- The try-with-resources block guarantees the buffer is flushed and the file handle is released even if an exception is thrown inside the loop.
Appending with BufferedWriter
To append rather than overwrite, pass StandardOpenOption.APPEND to the factory:
Passing both CREATE and APPEND is the idiomatic pattern: create the file if it does not exist, append to it if it does.
Choosing the Right API
Here is a practical decision rule:
- Small content, already in memory (a string, a list of lines, a byte array) → use
Files.writeStringorFiles.write. One line, no streams to manage. - Large content or incremental generation (loop, streaming results, building a report row by row) → use
BufferedWriterviaFiles.newBufferedWriter. The buffer prevents excessive system calls. - Binary data → use
Files.write(path, byte[])orFiles.newOutputStreamwrapped in aBufferedOutputStream.
BufferedWriter (8 KB buffer) those 10,000 calls collapse to roughly 5–10 flushes, which is orders of magnitude faster on spinning disks and still meaningfully faster on SSDs.
Exception Handling
All NIO.2 write methods throw IOException (a checked exception). You must either catch it or declare it in the method signature. A minimal pattern for a utility method:
In application code where you cannot propagate the checked exception (for example, inside a lambda), wrap it in an unchecked exception:
Summary
Use Files.writeString for the simplest case: a single string, one method call, UTF-8, done. Use Files.write when you have a list of lines or a byte array. Use BufferedWriter from Files.newBufferedWriter when you write incrementally or care about performance at scale. Control overwrite vs append with StandardOpenOption. Always use try-with-resources to guarantee the file is closed and the buffer is flushed.