☕ 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

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.