☕ Java

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.

Constructors — Charset, Append Mode, and the Overwrite Default

FileWriter has a constructor for every combination of (file reference) × (charset) × (append mode). File references can be either a String path or a File object. Charset defaults to the platform default in pre-Java-11 constructors and is explicit in Java-11 constructors. Append mode defaults to false (overwrite/truncate) and is controllable via a boolean append parameter. The overwrite default is a common source of bugs: opening an existing file with FileWriter(filename) silently truncates it to zero bytes before any write occurs. A file with 1MB of critical data becomes empty the moment FileWriter opens it, even if a subsequent write() throws an exception. This behavior is inherited from the underlying FileOutputStream — both FileWriter and FileOutputStream default to truncate-on-open. Append mode (FileWriter(file, true) or FileWriter(file, charset, true)) positions the file pointer at the end of the existing content. New writes are appended. The file is not truncated on opening. Append mode is atomic at the OS level for most file systems on POSIX-compliant systems — concurrent writers appending to the same file will not interleave their writes at the byte level, though they may interleave at the application level if not synchronized. Java 11 constructors add explicit charset: FileWriter(String fileName, Charset charset), FileWriter(String fileName, Charset charset, boolean append), FileWriter(File file, Charset charset), FileWriter(File file, Charset charset, boolean append). These are the correct constructors for portable code. The character encoding mechanics: FileWriter extends OutputStreamWriter, which holds an encoder that converts char sequences to byte sequences according to the charset. Characters that cannot be encoded in the target charset are replaced with the charset's replacement character (typically '?' for single-byte charsets or the Unicode replacement character U+FFFD for UTF-8). For critical data, use a charset that can represent all expected characters (UTF-8 for general text).
Java
// ── Pre-Java-11 constructors: implicit platform default charset ────────
FileWriter legacy1 = new FileWriter("output.txt");              // overwrite, default charset
FileWriter legacy2 = new FileWriter("output.txt", true);        // append,    default charset
FileWriter legacy3 = new FileWriter(new File("output.txt"));    // overwrite, default charset
FileWriter legacy4 = new FileWriter(new File("output.txt"), true); // append, default charset

// ── Java 11+: explicit charset ────────────────────────────────────────
FileWriter utf8Overwrite = new FileWriter("output.txt",  StandardCharsets.UTF_8);
FileWriter utf8Append    = new FileWriter("output.txt",  StandardCharsets.UTF_8, true);
FileWriter isoOverwrite  = new FileWriter(new File("output.txt"), StandardCharsets.ISO_8859_1);
FileWriter isoAppend     = new FileWriter(new File("output.txt"), StandardCharsets.ISO_8859_1, true);

// ── Overwrite (truncate) default — a common source of data loss ────────
File important = new File("important_data.txt");
// File has 1MB of data

try (FileWriter fw = new FileWriter(important, StandardCharsets.UTF_8)) {
    // File is NOW EMPTY — truncated on open, BEFORE any write
    // If fw.write() throws, the 1MB of data is gone permanently
    fw.write("replacement content");
}

// ── Safe overwrite pattern: write to temp, then rename ────────────────
File target = new File("important_data.txt");
File temp   = new File("important_data.txt.tmp");
try (FileWriter fw = new FileWriter(temp, StandardCharsets.UTF_8)) {
    fw.write("new content");
}   // temp file fully written and closed
temp.renameTo(target);   // atomic on most OS/filesystems — target replaced atomically

// ── Append mode: add to existing file ────────────────────────────────
// Log file: accumulate entries without truncating
try (FileWriter logger = new FileWriter("app.log", StandardCharsets.UTF_8, true)) {
    logger.write(LocalDateTime.now() + " INFO Application started
");
}   // existing log content preserved, new line added at end

// Multiple appends accumulate:
for (int i = 0; i < 5; i++) {
    try (FileWriter fw = new FileWriter("count.txt", StandardCharsets.UTF_8, true)) {
        fw.write("Line " + i + "
");
    }
}
// count.txt contains all 5 lines after 5 separate opens

// ── What charset to use? ──────────────────────────────────────────────
// UTF-8:          best default — encodes all Unicode, most common on modern systems
// ISO-8859-1:     Western European legacy files, 1 byte per char
// UTF-16:         fixed-width for BMP chars, rarely needed
// US-ASCII:       7-bit only, throws on any non-ASCII char
// windows-1252:   Windows legacy Western European files
System.out.println(StandardCharsets.UTF_8);   // always available, no checked exception

Write Methods, BufferedWriter Wrapping, and newLine()

FileWriter inherits three write methods from Writer. write(int c) writes a single character — the int is treated as a Unicode code point and the char value is encoded. write(String str) writes the entire string. write(String str, int offset, int count) writes a substring. write(char[] cbuf) writes the entire char array. write(char[] cbuf, int offset, int count) writes a portion of a char array. All these methods propagate to the underlying OutputStreamWriter's encoder immediately — there is no buffering in FileWriter itself. Because FileWriter is unbuffered, every write call potentially results in one or more system calls. Writing a multi-kilobyte String as a single write() call is reasonably efficient (one encode + write cycle). But writing many small strings or characters in a loop without buffering is slow. The fix is always the same: wrap FileWriter in a BufferedWriter. BufferedWriter adds a 8192-character buffer and two additional methods. newLine() writes the system-dependent line separator (System.lineSeparator() — on Unix, on Windows). This is preferable to hardcoding because it produces files with the platform's native line endings, which is important for interoperability with platform-specific tools. The second additional method is write(String s, int off, int len) — which is inherited from Writer but optimized in BufferedWriter to copy directly into the buffer without intermediate allocation. The newLine() vs decision: if writing files for cross-platform exchange (JSON, CSV, XML, HTTP), use universally — these formats specify Unix line endings or allow any. If writing files for native platform consumption (shell scripts, Windows batch files, configuration files read by native tools), use newLine() to get the platform's native separator. Files.writeString() and Files.write() always use the platform line separator when using writeln equivalents; explicit newline characters in the string are written as-is.
Java
// ── FileWriter write methods ──────────────────────────────────────────
try (FileWriter fw = new FileWriter("chars.txt", StandardCharsets.UTF_8)) {
    fw.write('A');                     // write single char (via int)
    fw.write(65);                      // same: 'A' (Unicode code point 65)
    fw.write("Hello, World!");         // write entire string
    fw.write("Hello, World!", 7, 5);   // write "World" (offset=7, count=5)

    char[] letters = {'J', 'a', 'v', 'a'};
    fw.write(letters);                 // write entire char array
    fw.write(letters, 0, 2);          // write "Ja" (offset=0, count=2)
}

// ── ALWAYS wrap FileWriter in BufferedWriter ───────────────────────────
// Unbuffered: one system call per write — extremely slow for many writes
try (FileWriter fw = new FileWriter("slow.txt", StandardCharsets.UTF_8)) {
    for (int i = 0; i < 100_000; i++) {
        fw.write("Line " + i + "
");  // each call goes to OS — 100,000 system calls
    }
}

// Buffered: system calls reduced to ~12 (100,000 lines × ~50 chars / 8192 buffer)
try (BufferedWriter bw = new BufferedWriter(
        new FileWriter("fast.txt", StandardCharsets.UTF_8))) {
    for (int i = 0; i < 100_000; i++) {
        bw.write("Line " + i);
        bw.newLine();   // platform-appropriate line ending
    }
}   // close() flushes remaining buffer — all lines written

// ── newLine() vs hardcoded 
 ─────────────────────────────────────────
try (BufferedWriter bw = new BufferedWriter(
        new FileWriter("platform.txt", StandardCharsets.UTF_8))) {
    bw.write("First line");
    bw.newLine();   // 

 on Windows, 
 on Unix — correct for platform tools

    bw.write("Second line");
    bw.write("
");   // always 
 — correct for cross-platform text/JSON/CSV/HTTP
}

// ── PrintWriter: adds printf and println over FileWriter ───────────────
try (PrintWriter pw = new PrintWriter(new BufferedWriter(
        new FileWriter("report.txt", StandardCharsets.UTF_8)))) {
    pw.println("Report generated: " + LocalDate.now());   // platform newLine
    pw.printf("Total: %d items, $%.2f%n", 42, 1234.56);  // formatted output
    pw.println();   // blank line
}

// ── Modern alternative: Files.writeString and Files.write ─────────────
// Write entire string at once (no intermediate FileWriter needed):
Files.writeString(Path.of("output.txt"), "Hello, UTF-8 World!",
    StandardCharsets.UTF_8,
    StandardOpenOption.CREATE,
    StandardOpenOption.TRUNCATE_EXISTING);

// Write lines (adds platform line separator after each):
List<String> lines = List.of("Line 1", "Line 2", "Line 3");
Files.write(Path.of("lines.txt"), lines, StandardCharsets.UTF_8);

// Append with Files.writeString:
Files.writeString(Path.of("log.txt"), "New entry
",
    StandardCharsets.UTF_8,
    StandardOpenOption.CREATE,
    StandardOpenOption.APPEND);

Related Topics in Java I/O

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.
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.
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.