JAVA / FILES, INPUT/OUTPUT AND SERIALIZATION
Writing text with writers and buffered output
Write text files with buffered writers: choose a charset, control append versus truncate, and understand why data only lands on disk at flush or close.
What you will learn
- Open text files with Files.newBufferedWriter and an explicit charset
- Rely on try-with-resources so close() flushes the buffer even when a write fails
- Pass CREATE and APPEND together to extend a file instead of truncating it
- Use PrintWriter for printf, and checkError() because it never throws IOException
Understanding Writing text with writers and buffered output
Writing text runs through a chain. Your String is copied into a BufferedWriter's char array, that array is handed to an encoder which turns characters into bytes for a specific charset, and those bytes go to an OutputStream that finally talks to the operating system. Every hop is cheap except the last one, so the buffer exists to turn thousands of tiny write calls into a handful of large ones. That is also why the file stays empty while the buffer is filling: the text is sitting in your process's memory, not in the file.
A buffer that is never drained is a buffer that is lost. close() drains it and then releases the file handle, which is why the writer belongs in a try-with-resources header: if a write throws halfway through, close still runs and the partial output survives. If the process instead ends without close, through System.exit or a kill, the trailing buffer content disappears with no exception at all, which makes the bug look like the last few lines were never written. flush() is for the case where another program has to see the text right now; calling it after every line is legal but gives back most of what buffering bought you.
Two decisions get baked into the bytes. The charset decides which bytes represent a character like é: Files.newBufferedWriter uses UTF-8 unless you say otherwise, while the old FileWriter used whatever the JVM default happened to be, which is how a file written on one machine turns into garbled characters on another. Line endings are the second decision, because newLine() writes System.lineSeparator(), so identical code produces CRLF on Windows and LF on Linux; when a format or a test demands LF, write \n literally. On top of any Writer you can put a PrintWriter for println and printf, at the price that it never throws IOException, swallowing the failure and setting a flag you must read with checkError().
Files.writeString and Files.write(path, lines) are the right tool when the whole text already exists in memory, since they open, write and close in one call; a Writer is what you want when the text is produced incrementally.
import java.io.BufferedWriter;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
public class WriteText {
public static void main(String[] args) throws IOException {
Path file = Files.createTempFile("notes", ".txt");
// Closed by hand so the flush timing is visible; newLine() writes
// System.lineSeparator(), so the sizes below assume a single LF byte.
BufferedWriter out = Files.newBufferedWriter(file, StandardCharsets.UTF_8);
out.write("first line");
out.newLine();
System.out.println("size before flush: " + Files.size(file));
out.flush();
System.out.println("size after flush: " + Files.size(file));
out.write("second line");
out.newLine();
out.close();
System.out.println("size after close: " + Files.size(file));
System.out.println(Files.readAllLines(file, StandardCharsets.UTF_8));
Files.delete(file);
}
}A Writer accepts characters, but the buffer decides when those characters become bytes in the file, so flush or close is what makes the text real.
Worked examples
Append instead of overwrite
Shows that the open options you pass replace the default truncating behaviour entirely.
import java.io.BufferedWriter;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
public class AppendMode {
public static void main(String[] args) throws IOException {
Path log = Files.createTempFile("log", ".txt");
try (BufferedWriter w = Files.newBufferedWriter(log, StandardCharsets.UTF_8)) {
w.write("run 1\n");
}
try (BufferedWriter w = Files.newBufferedWriter(log, StandardCharsets.UTF_8,
StandardOpenOption.CREATE, StandardOpenOption.APPEND)) {
w.write("run 2\n");
}
System.out.println(Files.readAllLines(log, StandardCharsets.UTF_8));
try (BufferedWriter w = Files.newBufferedWriter(log, StandardCharsets.UTF_8)) {
w.write("run 3\n");
}
System.out.println(Files.readAllLines(log, StandardCharsets.UTF_8));
Files.delete(log);
}
}Example explained
Line 1With no options the defaults are CREATE, TRUNCATE_EXISTING and WRITE, so the first block starts from an empty file.
Line 2CREATE plus APPEND replaces that default set, so run 2 lands after run 1 instead of on top of it.
Line 3APPEND on its own does not imply CREATE and throws NoSuchFileException on a missing file, which is why the two are paired.
Line 4The third block passes no options again, so the file is truncated and only run 3 remains.
PrintWriter hides IOException
Demonstrates formatted output through a PrintWriter and the error flag that replaces thrown exceptions.
import java.io.IOException;
import java.io.PrintWriter;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Locale;
public class SilentPrintWriter {
public static void main(String[] args) throws IOException {
Path file = Files.createTempFile("report", ".txt");
PrintWriter out = new PrintWriter(Files.newBufferedWriter(file, StandardCharsets.UTF_8));
out.printf(Locale.ROOT, "%-6s|%6.2f%n", "cpu", 12.5);
out.println("done");
out.close();
out.println("after close");
System.out.println("error flag: " + out.checkError());
System.out.println(Files.readAllLines(file, StandardCharsets.UTF_8));
Files.delete(file);
}
}Example explained
Line 1Locale.ROOT pins the decimal separator to a dot, so the file does not change shape on a machine whose locale writes 12,50.
Line 2%n emits System.lineSeparator(), while a literal \n in the format string would always be one LF byte.
Line 3The println after close() throws nothing: PrintWriter catches the IOException from the closed target and sets an internal flag.
Line 4checkError() reads that flag, so it is the only way the program can find out the write never happened.
What the buffer actually saves
Counts the calls that reach the underlying writer with and without buffering.
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.Writer;
public class CountingWrites {
static class Counting extends Writer {
int calls;
@Override public void write(char[] cbuf, int off, int len) { calls++; }
@Override public void flush() { }
@Override public void close() { }
}
public static void main(String[] args) throws IOException {
Counting plain = new Counting();
for (int i = 0; i < 5000; i++) plain.write("x");
System.out.println("unbuffered calls: " + plain.calls);
Counting target = new Counting();
try (BufferedWriter buffered = new BufferedWriter(target, 1024)) {
for (int i = 0; i < 5000; i++) buffered.write("x");
}
System.out.println("buffered calls: " + target.calls);
}
}Example explained
Line 1Writer.write(String) funnels into write(char[], int, int), so the unbuffered writer is called once per character.
Line 2BufferedWriter copies each character into its 1024-char array and touches the target only when the array is full.
Line 35000 characters give four full flushes plus a final one from close(), which is the five calls reported.
Line 4In a real chain each of those calls means an encode step and a write to the operating system, which is where the cost lives.
Important notes
Files.newBufferedWriter already returns a BufferedWriter, so wrapping it in another one just adds a second 8192-char copy for nothing.
Opening with CREATE and WRITE but without TRUNCATE_EXISTING starts writing at offset zero over the old content, so shorter new text leaves the tail of the old text behind.
Common mistakes
Letting the program end without close(): the last buffer-full is dropped silently, so the file appears cut off at an arbitrary character and no exception is ever reported.
Passing StandardOpenOption.APPEND alone, which does not imply CREATE, so the first run dies with NoSuchFileException instead of creating the file.
Calling write(someInt) expecting the number in the file: write(int) writes the character with that code, so write(65) stores A and write(10) quietly inserts a newline.
Try it yourself
Change, predict, then run
Write five numbered lines to a temp file with Files.newBufferedWriter inside try-with-resources, then reopen the same path with CREATE and APPEND to add a sixth line, and print Files.readAllLines to confirm all six survived.
Open the Java workspaceCheck your understanding
A program opens Files.newBufferedWriter, writes 200 short lines with write() and newLine(), then calls System.exit(0) without closing the writer. What is in the file?
- All 200 lines, because exiting the JVM flushes any open writer
- Only the lines that happened to fill the 8192-char buffer, and no exception is reported
- Nothing, because a BufferedWriter writes to the file only when close() is called
- All 200 lines, because newLine() flushes the buffer every time it is called
Show answer
The buffer drains automatically each time it fills, so whole chunks already reached the file; only the partly filled buffer dies with the process, and it does so without any error. Option 2 is tempting but treats close() as the only flush trigger, and newLine() merely appends the separator to the buffer, since flush-on-println is a PrintWriter option, not BufferedWriter behaviour.