PrintWriter is the convenience writer for formatted text output. It takes anything you give it (primitives, objects, null) and turns it into characters without making you call String.valueOf first, and it adds printf/format for column-aligned, currency-padded, decimal-trimmed output. This lesson covers what PrintWriter does, the full set of constructors, the print / println / printf / write method families, format specifiers, its quiet error-handling model, and how to chain it onto a BufferedWriter for buffered formatted output.
PrintWriter ExistsBufferedWriter is great at moving characters into a file efficiently, but its API is bare. The only methods are write(String), write(char[], off, len), write(int), and newLine(). If you want to write a numeric total, you have to convert it yourself:
Output (receipt.txt):
Every line is write(String + ...) followed by newLine(). The String.valueOf call on cartTotal is technically optional because + already triggers it, but the API has no direct method for "write this number, then end the line".
PrintWriter is the wrapper that adds those conveniences:
println(itemCount) writes an int and the line separator in one call.printf("Total: $%.2f%n", cartTotal) writes a formatted number with two decimal places, padded or aligned however you like.print(null) writes the four characters n, u, l, l instead of throwing NullPointerException.Here is the same receipt with PrintWriter:
Output (receipt.txt):
Same result, less ceremony. The printf line also gives you formatting control that BufferedWriter alone doesn't: forcing two decimal places (%.2f) is a one-character change, and aligning columns of receipts is a few characters more.
PrintWriter lives in java.io and has existed since Java 1.1. Its sibling PrintStream (which is what System.out is) does the same thing for byte streams.
PrintWriter has more constructors than most java.io classes because it accepts almost any sink: another Writer, an OutputStream, a File, or a path String. There's also an optional auto-flush flag and an optional Charset for the OutputStream and File/String overloads.
The diagram shows the most common chain. Your code calls PrintWriter methods. PrintWriter turns numbers and objects into strings and forwards the characters to the wrapped BufferedWriter. The buffered writer collects characters until its buffer fills, then forwards the chunk to FileWriter, which encodes characters into bytes and writes them to disk.
Here are the constructors you'll actually use, with the situations each one is for.
1. Wrap a `Writer`. This is the canonical form. Anything that already writes characters (a FileWriter, a BufferedWriter, a StringWriter) can be wrapped.
Output (orders.txt):
2. Wrap an `OutputStream`. Pass a byte stream and PrintWriter will encode characters for you using either the platform's default charset or the one you supply. This is what System.out (a PrintStream, the byte-stream cousin) does internally.
Output (orders.txt):
The three arguments are: the stream, the auto-flush flag, and the charset. The two-argument form (OutputStream, boolean) defaults to the platform charset; the three-argument form lets you pin it to UTF-8, which is what you almost always want.
3. From a `File`. Skip the boilerplate of creating a FileOutputStream yourself. PrintWriter(File) opens the file for writing and wraps it internally.
Output (orders.txt):
4. From a filename `String`. The shortest form. PrintWriter("orders.txt") is equivalent to PrintWriter(new File("orders.txt")).
Output (orders.txt):
The File and filename overloads throw FileNotFoundException if the file can't be created or opened for writing. That's the only checked exception PrintWriter itself throws; once it's constructed, the writing methods swallow IOException and surface the error through checkError() instead.
The full table of constructors:
| Constructor | Source | Auto-flush? | Charset control? |
|---|---|---|---|
PrintWriter(Writer) | Any Writer | No | Inherited |
PrintWriter(Writer, boolean autoFlush) | Any Writer | Yes | Inherited |
PrintWriter(OutputStream) | Any OutputStream | No | Platform default |
PrintWriter(OutputStream, boolean autoFlush) | Any OutputStream | Yes | Platform default |
PrintWriter(OutputStream, boolean, Charset) | Any OutputStream | Yes | Explicit |
PrintWriter(File) | A File | No | Platform default |
PrintWriter(File, Charset) | A File | No | Explicit |
PrintWriter(String fileName) | A path string | No | Platform default |
PrintWriter(String fileName, Charset) | A path string | No | Explicit |
Two practical notes on this table. First, the auto-flush flag only exists on the Writer and OutputStream overloads. The File and filename overloads can't be auto-flushed; if you need that behavior, wrap the file in a stream first. Second, prefer the Charset overloads for new code. Relying on the platform default charset means your file is encoded one way on a macOS laptop and another on a Linux production server, which is the kind of bug that surfaces unpredictably.
A flushed buffer is one that's been emptied: its contents have been pushed downstream toward the file. PrintWriter (like BufferedWriter) doesn't write characters to disk immediately. It holds them in a buffer and flushes when the buffer fills, when you call flush() explicitly, or when you call close().
Auto-flush is an opt-in mode where certain methods trigger a flush automatically. The constructor flag controls whether it's on:
When auto-flush is on, PrintWriter flushes after every call to println, printf, or format. It does not flush after print or write. The triggers are intentionally limited to the line-oriented and formatted methods, on the assumption that those are the natural "end of a logical record" boundaries.
The diagram captures the decision: auto-flush off means everything is buffered until close() or a manual flush(). Auto-flush on means line-oriented methods flush; raw print and write calls do not.
A concrete example shows why this matters for log files:
Output (events.log):
With auto-flush on, each println reaches disk before the next call returns. If the process dies between the first and second line, you still have the first line on disk. With auto-flush off, all three lines might still be sitting in the buffer when the JVM crashes.
Auto-flush forces a write to the underlying stream on every println/printf/format. For a single line that's negligible, but a tight loop that writes a million lines with auto-flush on does a million small writes instead of a handful of big ones. Turn auto-flush off for bulk writes and call flush() only when you need to.
For a one-shot receipt or report, leave auto-flush off and let close() (via try-with-resources) handle the final flush. For an event log or anything you might tail from another process, turn it on.
print and printlnprint and println are the workhorse methods. Each is overloaded for every primitive type plus String, char[], and Object. The difference between them is one line separator: println writes the system line separator after the value, print doesn't.
Output (overloads.txt):
Each overload calls String.valueOf on the value internally and writes the resulting characters. The last line shows the key behavior: passing null to println writes the literal four characters n, u, l, l instead of throwing NullPointerException. This is deliberate, and it's one reason PrintWriter is preferred over manually concatenating with +.
println() with no arguments writes just the line separator, which is the canonical way to insert a blank line:
Output (blank.txt):
A slightly more substantial example: writing a simple order receipt with mixed types.
Output (receipt.txt):
The mix of print and println is intentional. print writes the label without a line break, then println writes the value and ends the line. Each pair could be collapsed into one println with string concatenation; the split version is slightly more readable when one side is a label and the other side is a value of a different type.
printf and formatprintf and format are aliases for the same thing: write a format string with placeholders, plus arguments to fill those placeholders. format is the name from java.util.Formatter; printf is the name borrowed from C. Pick whichever you prefer; the implementation is identical, and both return the PrintWriter itself so they can be chained.
The general shape is out.printf(formatString, arg1, arg2, ...). The format string contains literal text mixed with format specifiers that start with %. Each specifier consumes one argument.
Output (summary.txt):
Three specifiers appear here: %s for the customer name, %d for the order count, and %.2f for the average order value rounded to two decimal places. Each line ends with %n, which is the platform-independent line separator. Use %n inside printf, not \n; \n is always a literal line feed character, while %n becomes \r\n on Windows and \n on macOS or Linux.
The big payoff over print + concatenation is alignment. Building a column of order IDs and totals is painful with +, but straightforward with printf:
Output (orders.txt):
The columns line up because %-12s pads each order ID with spaces on the right to width 12, and %9.2f pads each total with spaces on the left to width 9 with two decimal places. Producing that with + would require manually counting spaces.
A full format specifier has the shape %[flags][width][.precision]conversion. Everything except % and the conversion character is optional.
The cyan box is mandatory, the green box is mandatory, and the orange boxes are all optional. %s is a valid specifier (no flags, width, or precision). %-12.5s is also valid (left-align, minimum width 12, maximum 5 characters of the value).
Conversion characters. The conversion tells the formatter what kind of value to expect.
| Conversion | Use for | Example specifier | Example output |
|---|---|---|---|
s | Any value, via String.valueOf | %s | Maria |
d | Integers (byte/short/int/long) | %d | 42 |
f | Floating-point (float, double) | %f | 3.140000 |
e | Scientific notation | %e | 3.140000e+00 |
b | Boolean (true/false, null becomes false) | %b | true |
c | Character | %c | X |
x | Hexadecimal integer | %x | ff |
o | Octal integer | %o | 377 |
n | Platform line separator | %n | (newline) |
% | A literal percent sign | %% | % |
Width. A positive integer between the flags and precision. It's the minimum number of characters the field occupies; values shorter than that are padded with spaces on the left (right-justified) by default.
Each value is padded with spaces to fill 10 characters total, and the values are right-aligned.
The `-` flag (left-justify). Add - between % and the width to left-align instead.
Now the values hug the left edge and the padding is on the right.
The `0` flag (zero-pad numbers). For numeric conversions, 0 pads with zeros instead of spaces. Common for fixed-width IDs.
Each order ID gets exactly six digits with leading zeros. Useful when you want IDs to sort correctly as text.
The `,` flag (group digits). Add , for locale-appropriate thousands separators.
The comma flag is the easiest way to make money figures readable without writing a custom formatter.
Precision. After a ., an integer that means different things depending on the conversion:
%f, it's the number of digits after the decimal point. %.2f rounds to two decimals.%s, it's the maximum number of characters from the value to use. %.5s truncates strings longer than five characters.%e, it's also the number of digits after the decimal point.%.2f rounds 3.14159 to two decimals (3.14). %.5f pads 3.14 with trailing zeros to reach five decimal digits (3.14000). %.5s truncates Notebook to its first five characters (Noteb) and leaves Pen alone because it's already shorter.
Combining everything. Width and precision can both appear in one specifier. %10.2f means "at least 10 characters wide, with 2 decimal places". %-12.5s means "left-aligned, at least 12 characters wide, at most 5 characters from the value".
The first line right-aligns 3.14 in a 10-character field. The second left-aligns it. The third truncates to 5 characters (Noteb), then pads to width 12 with spaces.
To see how the pieces fit together, an invoice with aligned product columns, currency formatting, and a total line.
Output (invoice.txt):
Every column lines up. The product names are left-aligned in a 22-character field. The quantity is right-aligned in a 5-character field. Prices use %10.2f for the unit price and %12.2f for the subtotal, both right-aligned with two decimal places. The grand total uses %11.2f instead of %12.2f because the dollar sign is written separately and steals one character of width.
Concatenating with + inside a loop builds a new String per iteration, allocating throw-away StringBuilder instances along the way. printf writes directly to the PrintWriter's buffer without intermediate strings. For a 100-line invoice, neither matters; for a daily-sales-report with a million rows, printf is meaningfully kinder to the garbage collector.
write vs printPrintWriter inherits write from its parent Writer. There are five overloads, none of which do exactly what print does.
| Method | What it writes |
|---|---|
write(int c) | The single character whose code is c (low 16 bits) |
write(char[] buf) | The entire character array |
write(char[] buf, int off, int len) | A slice of the character array |
write(String s) | The entire string |
write(String s, int off, int len) | A slice of the string |
The trap is write(int). If you call out.write(65), you get the character A (because 65 is the Unicode code point for A). If you call out.print(65), you get the two characters 6 and 5.
For strings, write and print are interchangeable. For integers, they're completely different: write(int) treats the argument as a Unicode code point, while print(int) treats it as a number to convert to text. Mixing them up produces files full of unexpected characters.
The general rule: prefer print / println / printf for normal output, and use write only when you specifically need to dump a char[] slice or the raw write(int) semantics.
checkErrorPrintWriter is unusual in java.io. Its print, println, printf, format, and write methods do not throw `IOException`. If something goes wrong while writing (the disk fills up, the underlying stream is closed, a network connection drops), the exception is caught internally and the writer sets a private error flag.
The flag stays set forever once it's been raised. You query it with checkError(), which returns true if the writer has ever encountered an error (and flushes the writer as a side effect).
In normal operation checkError returns false and the code looks like it does here. The call has to be made explicitly. BufferedWriter, by contrast, throws IOException from write and newLine, so a missed error is hard to ignore.
There are two practical takeaways:
new PrintWriter("/path/that/does/not/exist.txt") and the directory is missing, you get the checked exception.A version of the daily report that handles both kinds of failure:
Output (daily-sales-report.txt):
The try-with-resources block handles closing. The catch block handles the file-not-found case at construction time. The checkError call catches errors that happened during writing. That's the three-part safety net.
BufferedWriterPrintWriter formats. BufferedWriter buffers. They do different jobs, and the canonical pattern in production code is to chain them: PrintWriter -> BufferedWriter -> FileWriter -> file.
PrintWriter already has an internal buffer, and for small files (a single receipt, a config snapshot) wrapping BufferedWriter underneath adds nothing. The reason to do it anyway is two-fold. First, BufferedWriter has a configurable buffer size (8 KB by default), which matters for large outputs. Second, putting BufferedWriter in the chain makes the buffering behavior explicit.
Output (top-products.txt):
The chain reads outside-in: your code talks to PrintWriter, which formats and forwards characters to BufferedWriter, which accumulates them and forwards chunks to FileWriter, which encodes characters to bytes for the file. try-with-resources closes the outermost writer, which cascades the close down the chain so every layer flushes and releases its resources.
A small subtle point: closing PrintWriter flushes its own buffer, then calls close() on the wrapped writer. The wrapped writer's close() does the same down the line. You only need one try-with-resources declaration for the whole chain because the outer wrapper owns the closing of the inner wrappers.
For most realistic e-commerce reporting code, this chain is a good default. The auto-flush flag is off (the default), so writes are buffered for performance; close() flushes everything when the block exits.
System.out and PrintStreamSystem.out is the JVM's standard output stream. Its declared type is PrintStream, not PrintWriter. PrintStream is the byte-stream cousin of PrintWriter: it has the same print, println, printf, format, and write methods, the same auto-flush behavior, and the same swallow-exceptions-set-error-flag model.
Two practical differences matter:
| Feature | PrintWriter | PrintStream |
|---|---|---|
| Underlying sink | Writer (characters) | OutputStream (bytes) |
| Default charset | Platform default | Platform default |
| Auto-flush trigger | println/printf/format | Same, plus a newline byte in write |
| Existed since | Java 1.1 | Java 1.0 |
The "auto-flush on newline byte" detail is why System.out.println reaches the console immediately even though System.out is auto-flush by default. For your own files, the difference between writing through a PrintWriter and through a PrintStream is mostly which encoding step happens where. Use PrintWriter for new code that produces text files; PrintStream is fine if you specifically want to write to an OutputStream directly.
Calls like System.out.printf("Total: %.2f%n", 49.95) already use PrintStream's formatting machinery. Switching to PrintWriter for file output is the same API in a slightly different package.
BufferedWriterA short summary of when to use which:
| Need | Best choice |
|---|---|
| Write large amounts of plain text, line by line | BufferedWriter |
| Write formatted columns, numbers, dates | PrintWriter |
Need every error surfaced as IOException | BufferedWriter |
| Need null-safe printing of arbitrary objects | PrintWriter |
| Need flush-after-each-line for a live log | PrintWriter with auto-flush on |
| Both formatting and large output | PrintWriter wrapping BufferedWriter |
The pragmatic rule: if the code calls String.format and then writes the result to a BufferedWriter, use PrintWriter instead. If explicit IOException propagation from every write call is required, use BufferedWriter instead.
10 quizzes