☕ Java

PrintWriter

PrintWriter is a character-based output class that wraps any Writer or OutputStream and adds convenience methods for printing formatted representations of all Java primitive types, strings, and objects. Its defining characteristic is that none of its print(), println(), and printf() methods throw checked IOException — errors are silently swallowed and can only be detected after the fact by calling checkError(). This makes PrintWriter easy to use interactively and in situations where I/O failure is genuinely unrecoverable (writing to System.out, generating diagnostic output), but makes it dangerous for critical data writing where exceptions must be caught and handled. PrintWriter can auto-flush on println(), printf(), and format() calls when constructed with autoFlush=true, which is useful for interactive console output and network protocol streams. Its printf() and format() methods delegate to java.util.Formatter, enabling C-style formatted output with full locale awareness. This entry covers all constructor variants and their autoFlush and buffering behavior, every print/println/printf method, the checkError() error detection model, the difference between PrintWriter and PrintStream, charset handling, and when PrintWriter is the right choice versus BufferedWriter.

Constructors — Auto-flush, Buffering, and Charset

PrintWriter has a large set of constructors that fall into two categories: those wrapping a Writer, and those wrapping an OutputStream (or file name/File). The Writer-based constructors are new PrintWriter(Writer out) and new PrintWriter(Writer out, boolean autoFlush). These do not add any buffering — if the underlying Writer is unbuffered (like FileWriter), writes go to the OS on every call. In practice, you always pass a BufferedWriter to these constructors: new PrintWriter(new BufferedWriter(new FileWriter(...))). The OutputStream-based constructors are new PrintWriter(OutputStream out) and new PrintWriter(OutputStream out, boolean autoFlush). These constructors internally create an OutputStreamWriter (with the platform default charset — a legacy behavior) and wrap it in a BufferedWriter, so the resulting PrintWriter is buffered. Since Java 10, new PrintWriter(OutputStream out, boolean autoFlush, Charset charset) adds explicit charset control. The File-based and String-based constructors — new PrintWriter(File file), new PrintWriter(String fileName), new PrintWriter(File file, String charsetName), new PrintWriter(String fileName, String charsetName), and since Java 10 the Charset-accepting variants — all open or create the named file, wrap it in a BufferedWriter with explicit (or default) charset. They throw FileNotFoundException (a checked exception) at construction time if the file cannot be opened. The autoFlush parameter controls whether println(), printf(), and format() trigger an automatic flush() after they complete. write() and print() do NOT trigger auto-flush even when autoFlush is true. This distinction is important for network protocols: writing a complete line with println() will flush automatically if autoFlush is enabled, making the data immediately available to the remote end. The default is autoFlush=false, which is appropriate for file output where explicit or close-triggered flushing is sufficient.
Java
// ── Writer-based constructors ─────────────────────────────────────────
// No buffering added — FileWriter is unbuffered, every write hits OS:
PrintWriter pw1 = new PrintWriter(new FileWriter("out.txt", StandardCharsets.UTF_8));

// Correct: wrap FileWriter in BufferedWriter first:
PrintWriter pw2 = new PrintWriter(
    new BufferedWriter(new FileWriter("out.txt", StandardCharsets.UTF_8)));

// With autoFlush — println/printf/format trigger flush automatically:
PrintWriter pw3 = new PrintWriter(
    new BufferedWriter(new FileWriter("out.txt", StandardCharsets.UTF_8)), true);

// ── OutputStream-based constructors ──────────────────────────────────
// Internally creates OutputStreamWriter(platform charset) + BufferedWriter:
PrintWriter pw4 = new PrintWriter(System.out);                      // stdout
PrintWriter pw5 = new PrintWriter(System.out, true);                // autoFlush
PrintWriter pw6 = new PrintWriter(new FileOutputStream("out.txt")); // file

// Java 10+: explicit charset:
PrintWriter pw7 = new PrintWriter(
    new FileOutputStream("out.txt"), true, StandardCharsets.UTF_8);

// ── File/String constructors ──────────────────────────────────────────
try {
    // Opens file, creates if absent, truncates if exists:
    PrintWriter pw8  = new PrintWriter("output.txt");                      // default charset
    PrintWriter pw9  = new PrintWriter(new File("output.txt"));            // default charset
    PrintWriter pw10 = new PrintWriter("output.txt", "UTF-8");             // explicit charset
    PrintWriter pw11 = new PrintWriter(new File("output.txt"), "UTF-8");   // explicit charset
    // Java 10+:
    PrintWriter pw12 = new PrintWriter("output.txt", StandardCharsets.UTF_8); // Charset object
} catch (FileNotFoundException e) {
    System.err.println("Cannot open file: " + e.getMessage());
}

// ── Preferred modern construction ─────────────────────────────────────
// Most explicit: BufferedWriter from Files, explicit charset, explicit options
try (PrintWriter pw = new PrintWriter(Files.newBufferedWriter(
        Path.of("output.txt"),
        StandardCharsets.UTF_8,
        StandardOpenOption.CREATE,
        StandardOpenOption.TRUNCATE_EXISTING))) {
    pw.println("UTF-8 with explicit options");
}

// ── autoFlush: only println/printf/format trigger it, NOT print/write ─
PrintWriter autoFlushing = new PrintWriter(System.out, true);
autoFlushing.print("not flushed");     // NO auto-flush — stays in buffer
autoFlushing.println("flushed now");   // println → auto-flush triggered
autoFlushing.printf("also flushed%n"); // printf → auto-flush triggered

Print Methods, printf/format, and Error Handling

PrintWriter provides print(T), println(T), and printf/format methods for all types. print() and println() have overloads for boolean, char, int, long, float, double, char[], Object, and String. println() appends the platform line separator (System.lineSeparator()) after the value and, if autoFlush is true, flushes the stream. print() never flushes. For Object arguments, print() calls String.valueOf(obj), which calls obj.toString() or returns "null" if obj is null — PrintWriter never throws NullPointerException for null arguments to print/println. printf(String format, Object... args) and format(String format, Object... args) are identical — format() is the name used in the Formatter API; printf() is the traditional C name. Both delegate to a java.util.Formatter instance and support the full format string specification: %d (integer), %f (float), %s (string), %n (platform newline — prefer over in format strings), %b (boolean), %c (char), %x (hex), %e (scientific notation), %t (date/time), and more. Width, precision, left/right alignment, zero-padding, and locale-sensitive formatting are all supported. The silent error model: every write, print, println, printf, and format call on PrintWriter is internally wrapped in a try-catch that catches all IOException instances. The caught exception sets an internal error flag. No exception propagates to the caller. This means a disk-full condition, a closed stream, a broken network connection, or any other I/O failure during a println() is silently ignored. The only way to detect that an error occurred is to call checkError(), which flushes the stream and returns true if any error has been recorded since construction or since the last call to clearError(). The silent error model is acceptable for System.out and System.err (console output where errors are unrecoverable) and for diagnostic/logging output where the application should continue operating even if log writing fails. It is unacceptable for writing business data, configuration files, reports, or any file whose contents matter for correctness. For those use cases, use BufferedWriter directly, where IOException propagates and must be caught.
Java
// ── print() overloads for all types ──────────────────────────────────
try (PrintWriter pw = new PrintWriter(Files.newBufferedWriter(
        Path.of("out.txt"), StandardCharsets.UTF_8))) {

    pw.print(true);          // "true"
    pw.print('A');           // "A"
    pw.print(42);            // "42"
    pw.print(3.14f);         // "3.14"
    pw.print(3.14159265);    // "3.141592653589793"
    pw.print(9_876_543_210L);// "9876543210"
    pw.print(new char[]{'H','i'}); // "Hi"
    pw.print((Object)null);  // "null" — no NullPointerException
    pw.print("Hello");       // "Hello"
}

// ── println() adds line separator ─────────────────────────────────────
try (PrintWriter pw = new PrintWriter(Files.newBufferedWriter(
        Path.of("lines.txt"), StandardCharsets.UTF_8))) {

    pw.println("Line 1");    // "Line 1" + System.lineSeparator()
    pw.println(42);          // "42" + System.lineSeparator()
    pw.println();            // just System.lineSeparator() (blank line)
}

// ── printf/format: C-style formatted output ───────────────────────────
try (PrintWriter pw = new PrintWriter(Files.newBufferedWriter(
        Path.of("report.txt"), StandardCharsets.UTF_8))) {

    // Basic formatting:
    pw.printf("Name: %-20s Age: %3d%n", "Alice", 30);     // left-aligned name, right-aligned age
    pw.printf("Pi: %.5f%n", Math.PI);                      // 5 decimal places: 3.14159
    pw.printf("Hex: %08X%n", 255);                         // zero-padded hex: 000000FF
    pw.printf("Sci: %e%n", 1_234_567.89);                  // 1.234568e+06
    pw.printf("Bool: %b%n", null);                         // "false" (nullfalse for %b)

    // %n vs 
: use %n for portable platform newline in format strings:
    pw.printf("Line 1%nLine 2%n");    // 

 on Windows, 
 on Unix
    pw.printf("Line 1
Line 2
");    // always for cross-platform formats

    // Locale-specific number formatting:
    pw.printf(Locale.GERMANY, "Preis: %.2f EUR%n", 1234.56);  // "1234,56" (comma decimal)
    pw.printf(Locale.US,      "Price: $%.2f%n",    1234.56);  // "1234.56" (period decimal)

    // Date/time:
    pw.printf("Date: %tY-%tm-%td%n", LocalDate.now(), LocalDate.now(), LocalDate.now());
}

// ── Silent error model: checkError() ─────────────────────────────────
PrintWriter riskWriter = new PrintWriter(new FileOutputStream("risk.txt"));
riskWriter.println("Important data");
// Simulate: file system becomes unavailable after open (in practice this would happen
// on disk full, network share disconnect, etc.)
riskWriter.println("More important data");
riskWriter.println("Critical record");

// checkError() flushes, then reports accumulated errors:
if (riskWriter.checkError()) {
    System.err.println("WARNING: Write errors occurred — data may be incomplete");
    // But it's too late to know WHICH writes failed
}

// ── Compare: BufferedWriter throws immediately ────────────────────────
try (BufferedWriter bw = Files.newBufferedWriter(Path.of("safe.txt"))) {
    bw.write("Important data");
    bw.newLine();
    // If disk is full: IOException thrown HERE — caller handles it immediately
} catch (IOException e) {
    System.err.println("Write failed at known point: " + e.getMessage());
    // Recovery action: retry, alert, abort cleanly
}

PrintWriter vs PrintStream, System.out, and When to Use Each

PrintWriter and PrintStream are closely related but distinct. PrintStream is a byte-oriented OutputStream subclass and is the type of System.out and System.err. It encodes characters to bytes using the platform default encoding (or an explicit charset since Java 10). PrintWriter is a character-oriented Writer subclass with no byte encoding built in — it delegates to an underlying Writer that handles encoding. PrintWriter is the preferred choice for character-based output because it properly separates the concern of character generation from byte encoding: the encoding policy is set once when the underlying OutputStreamWriter or FileWriter is created. System.out and System.err are PrintStream instances set up by the JVM. Writing to them via System.out.println() is PrintStream behavior: bytes are written using the JVM's stdout encoding (UTF-8 on modern systems, potentially legacy encodings on Windows). For performance-critical logging to stdout, wrapping System.out in a BufferedWriter/PrintWriter chain adds buffering: new PrintWriter(new BufferedWriter(new OutputStreamWriter(System.out, StandardCharsets.UTF_8))). This reduces the number of native write() calls at the cost of delayed output (requires explicit flush() or println() with autoFlush). PrintWriter is the correct tool for: generating output to System.out/System.err in test code and scripts; HTTP response writing in servlet-based web frameworks (HttpServletResponse.getWriter() returns a PrintWriter); generating formatted diagnostic output in debugging tools and CLIs; simple file generation where the silent-error model is acceptable. It is the wrong tool for: writing business data where write errors must be detected and handled; any file where partial writes must be detected and retried; any output where checkError() being false is not a sufficient correctness guarantee. The interaction between PrintWriter and String.format(): both produce formatted strings but in different ways. String.format() returns a String that can then be written via bw.write(). PrintWriter.printf() writes directly without creating an intermediate String object. For high-throughput logging, PrintWriter.printf() is slightly more efficient. For composing complex strings that will be used multiple times, String.format() or StringBuilder is more appropriate.
Java
// ── PrintWriter vs PrintStream ────────────────────────────────────────
// System.out is a PrintStream (byte-based, platform encoding):
System.out.println("Hello");    // encodes "Hello" to bytes using platform charset
System.out.printf("Pi=%.2f%n", Math.PI);

// PrintWriter wrapping System.out (character-based, explicit charset):
PrintWriter stdout = new PrintWriter(
    new BufferedWriter(new OutputStreamWriter(System.out, StandardCharsets.UTF_8)),
    true);   // autoFlush: println triggers immediate flush
stdout.println("Hello from PrintWriter");
stdout.printf("Pi=%.2f%n", Math.PI);
stdout.flush();   // explicit flush for non-println writes

// ── Servlet response pattern (javax.servlet / jakarta.servlet) ────────
// HttpServletResponse.getWriter() returns a PrintWriter:
@Override
public void doGet(HttpServletRequest req, HttpServletResponse resp)
        throws IOException {
    resp.setContentType("text/html; charset=UTF-8");
    resp.setCharacterEncoding("UTF-8");
    try (PrintWriter out = resp.getWriter()) {
        out.println("<!DOCTYPE html>");
        out.println("<html><body>");
        out.printf("<h1>Hello, %s!</h1>%n", req.getParameter("name"));
        out.println("</body></html>");
    }
    // checkError() can detect write failures to the client (e.g., client disconnected):
    // if (out.checkError()) log.warn("Client disconnected during response");
}

// ── CLI tool: PrintWriter for formatted console output ────────────────
public static void printTable(List<DataRow> rows) {
    try (PrintWriter out = new PrintWriter(
            new BufferedWriter(new OutputStreamWriter(System.out, StandardCharsets.UTF_8)),
            false)) {  // no autoFlush — print header and all rows, then flush once

        out.printf("%-20s %10s %15s%n", "Name", "Count", "Percentage");
        out.printf("%-20s %10s %15s%n", "-".repeat(20), "-".repeat(10), "-".repeat(15));
        for (DataRow row : rows) {
            out.printf("%-20s %,10d %14.2f%%%n",
                row.name(), row.count(), row.percentage());
        }
        out.flush();   // one flush for all output
    }
}

// ── String.format() vs printf() performance trade-off ─────────────────
// String.format() creates intermediate String — useful when string is reused:
String formatted = String.format("User: %-20s ID: %08d", user.name(), user.id());
logger.info(formatted);     // reused for logging
pw.println(formatted);      // and for output

// printf() writes directly — no intermediate String — better for write-once output:
pw.printf("User: %-20s ID: %08d%n", user.name(), user.id());

// ── Wrapping PrintWriter in try-with-resources ────────────────────────
// PrintWriter.close() closes the underlying Writer — always use try-with-resources:
try (PrintWriter pw = new PrintWriter(Files.newBufferedWriter(
        Path.of("output.txt"), StandardCharsets.UTF_8))) {
    pw.println("Data written safely");
    pw.printf("Formatted: %d%n", 42);
}   // close() flushes BufferedWriter, closes FileOutputStream

// Check for errors after close (unusual pattern — only for critical files):
try (PrintWriter pw = new PrintWriter(Files.newBufferedWriter(
        Path.of("critical.txt"), StandardCharsets.UTF_8))) {
    pw.println("Critical data");
    pw.println("More critical data");
    boolean hadError = pw.checkError();   // checkError before close to detect write failures
    if (hadError) throw new IOException("PrintWriter reported write error");
}

Related Topics in Java I/O

FileWriter
FileWriter is a convenience class for writing characters to a file, extending OutputStreamWriter with a FileOutputStream underneath. It encodes Java characters into bytes using the platform's default charset (or an explicit charset since Java 11) and writes them to a named file or File object. FileWriter supports two modes: overwrite (the default, which truncates the file to zero length on opening) and append (which positions the write pointer at the end of the existing file content). Like FileReader, FileWriter is unbuffered — each write() call propagates immediately to the underlying FileOutputStream, triggering system calls. In practice, FileWriter is almost always wrapped in a BufferedWriter to batch writes into efficient OS calls. The charset trap is identical to FileReader: pre-Java-11 constructors use the platform default charset silently, which causes portability problems; Java 11 constructors accept an explicit Charset. This entry covers all constructor variants with their charset and append semantics, the write methods and their character vs string behavior, newLine() in BufferedWriter, the flush/close contract, and the preferred modern alternatives.
BufferedReader
BufferedReader wraps any Reader with an in-memory character buffer, dramatically reducing system calls for character-by-character or line-by-line reading. Its defining method is readLine(), which reads a complete line of text terminated by \n, \r, or \r\n and returns it without the terminator, or returns null at end-of-file. Beyond buffering, BufferedReader also exposes a lines() method (Java 8+) that returns a lazy Stream<String> of lines, enabling the full Stream API for file processing without loading the entire file into memory. BufferedReader supports mark/reset with a caller-specified readAheadLimit. It is obtained either by wrapping a Reader (new BufferedReader(new FileReader(...))) or directly from Files.newBufferedReader(path, charset), which is the preferred idiom in modern Java. This entry covers construction and buffer sizing, all read methods and their contracts, readLine() edge cases (empty lines, last line without terminator), the lines() stream and its relationship to reader lifecycle, mark/reset semantics with readAheadLimit, and the use of BufferedReader as a base for protocol parsing.
BufferedWriter
BufferedWriter wraps any Writer with an in-memory character buffer, reducing system calls by accumulating characters until the buffer fills, flush() is called, or close() is called. It adds two capabilities not present in Writer: newLine(), which writes the platform-specific line separator, and an optimized write(String, int, int) that avoids creating a char[] copy by writing directly from the String. BufferedWriter is the standard output partner to BufferedReader — together they provide efficient line-by-line text file processing. It is constructed either by wrapping a Writer (new BufferedWriter(new FileWriter(...))) or via Files.newBufferedWriter(path, charset, options), the modern idiomatic alternative. Like all buffered streams, correct usage requires try-with-resources to guarantee that buffered data is flushed and the file is closed even when exceptions occur. This entry covers construction and buffer sizing, all write methods and their interaction with the buffer, newLine() and its platform behavior, flush semantics including when explicit flush is necessary, the difference between close() and flush(), and performance patterns for high-throughput text writing.
DataInputStream
DataInputStream wraps any InputStream and adds methods for reading Java primitive types in a machine-independent binary format. It reads boolean, byte, short, int, long, float, double, and char values from the underlying stream using fixed byte widths and big-endian byte order. The big-endian, fixed-width encoding is identical to the format written by DataOutputStream, making the two classes the natural pair for serializing and deserializing primitive data across files, network connections, or inter-process pipes. DataInputStream also provides readFully(), which blocks until exactly the specified number of bytes have been read — filling a buffer completely rather than returning a partial read as InputStream.read() may do. readUTF() reads a string encoded in a modified UTF-8 format (a two-byte length prefix followed by the encoded string bytes) that was written by DataOutputStream.writeUTF(). DataInputStream is unbuffered, so it should always be wrapped inside a BufferedInputStream for performance. This entry covers the full method API, the big-endian byte order contract, readFully() vs read() semantics, the modified UTF-8 format and its limitations, end-of-file detection, and composition patterns for binary protocol parsing.