☕ 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

I/O Basics
Java I/O is built on a small set of abstract concepts that underlie every I/O operation in the language: streams, readers, writers, channels, and buffers. A stream is a sequential flow of data — bytes moving from a source to a destination one at a time or in chunks. Java organizes I/O around two fundamental distinctions: byte I/O (reading and writing raw bytes, the universal representation that everything ultimately reduces to) and character I/O (reading and writing text encoded in a specific character set, with automatic encoding and decoding). The original java.io package, introduced in Java 1.0, provides stream-based I/O through four abstract base classes: InputStream, OutputStream, Reader, and Writer. The java.nio package, introduced in Java 1.4, adds a channel-and-buffer model for non-blocking and memory-mapped I/O. The java.nio.file package, introduced in Java 7 as part of NIO.2, provides a modern, comprehensive file system API that supersedes much of java.io.File. This entry covers the conceptual model of streams and their abstract base classes, the decorator pattern that underlies Java I/O class hierarchy, the source-processor-sink taxonomy of stream classes, blocking versus non-blocking I/O, buffering and why it is almost always necessary, the standard I/O streams (System.in, System.out, System.err), and the resource management contract that every I/O class must satisfy.
Byte Streams
Byte streams are the fundamental I/O abstraction in Java for reading and writing raw binary data. InputStream and OutputStream are the abstract base classes for all byte-oriented I/O, and their concrete subclasses cover every byte-level data source and destination: files, byte arrays in memory, network sockets, pipes between threads, and process standard streams. The critical read() contract — returning an int from 0 to 255 for valid bytes and -1 for end-of-stream — is the foundation of all stream-based binary processing. Byte streams do not perform character encoding or decoding; every byte is passed through as-is, making them correct for binary formats (images, audio, archives, serialized data, protocol buffers), and incorrect for text unless the encoding is explicitly managed. This entry covers the complete InputStream and OutputStream APIs, every major concrete byte stream class and its use case, DataInputStream and DataOutputStream for structured binary I/O, the mark/reset mechanism, available() and its correct interpretation, skipping and transferTo, and ObjectInputStream and ObjectOutputStream for Java serialization.
Character Streams
Character streams, represented by the Reader and Writer abstract base classes, handle text data by abstracting away the encoding and decoding between Java's internal char/String representation (UTF-16) and the byte encoding used in files and network connections. Where byte streams treat data as raw octets, character streams treat data as Unicode characters, handling multi-byte sequences transparently according to a specified Charset. InputStreamReader and OutputStreamWriter are the bridge classes that connect byte streams to character streams, applying charset encoding on write and decoding on read. BufferedReader adds line-at-a-time reading via readLine() and multi-character buffering. PrintWriter adds print/println/printf formatting output. StringReader and StringWriter enable in-memory character stream operations on String data. This entry covers the complete Reader and Writer APIs, charset handling and the consequences of using the wrong charset, the complete class hierarchy of character streams with the use case for each, BufferedReader.readLine() semantics and the lines() stream, the bridge classes in depth, character encoding best practices, and the interaction between character streams and Java's String.lines() and Files.readString()/writeString() alternatives.
File Handling
File handling in Java spans two generations of API: the legacy java.io.File class introduced in Java 1.0, and the modern java.nio.file package (NIO.2) introduced in Java 7 with its Path interface, Files utility class, and FileSystem abstraction. The File class represents a file or directory path as an abstract pathname and provides methods for querying metadata, listing directory contents, creating and deleting files, and basic path manipulation. Its limitations — no symbolic link support, inconsistent error reporting (methods return boolean instead of throwing exceptions), no atomic operations, limited metadata access, and performance issues for large directory traversals — motivated the complete redesign in NIO.2. The Path interface and Files class cover all functionality of File with better exception handling, symbolic link support, atomic operations, rich metadata via BasicFileAttributes, efficient directory walking with Files.walk() and Files.walkFileTree(), file watching with WatchService, and a provider model for custom file system implementations. This entry covers the complete File API and its limitations, the NIO.2 Path and Files APIs, directory traversal strategies, file watching, temporary files, and best practices for cross-platform path handling.