☕ Java

FileOutputStream

FileOutputStream is a concrete OutputStream subclass that writes raw bytes to a file on the file system. It is the primary mechanism for creating or overwriting binary files in Java. FileOutputStream opens a file, holds an OS file descriptor for writing, and writes bytes by delegating to the OS write() system call. Like FileInputStream, FileOutputStream performs no buffering — every write() call is a system call — making BufferedOutputStream an essential wrapper. FileOutputStream can be constructed from a String path, a File object, or a FileDescriptor, with an optional append mode flag that opens the file for appending rather than truncating. This entry covers all constructor forms and their exact file creation behavior, append mode semantics and atomicity, the flush/close contract and data loss risks, BufferedOutputStream as a mandatory wrapper, writing structured binary data with DataOutputStream, the difference between flush() and OS-level fsync, atomic write patterns using temporary files, and NIO.2 alternatives for common file writing tasks.

Construction, Append Mode, and the Truncation Contract

FileOutputStream has five constructors that differ in whether they take a String path or File object, and whether they open in append mode. FileOutputStream(String name) and FileOutputStream(File file) open the file for writing, truncating it to zero length if it exists, or creating it if it does not exist. FileOutputStream(String name, boolean append) and FileOutputStream(File file, boolean append) open in append mode when append is true: if the file exists, subsequent writes go to the end of the file rather than overwriting from the start; if the file does not exist, it is created. FileOutputStream(FileDescriptor fd) wraps an existing file descriptor. The creation and truncation behavior at construction time is an important distinction from FileInputStream. FileOutputStream's constructor immediately either creates the file or truncates it to zero length and positions the write pointer at byte 0. This means that opening a FileOutputStream on an existing file irreversibly destroys its contents even before any write() call. This is a common mistake in code that constructs a FileOutputStream in one location and then decides not to write to it — the file is already gone. If you need to test whether writing will succeed before committing to the write, check file existence and permissions before constructing the FileOutputStream. Append mode — FileOutputStream(file, true) — opens the file without truncation and positions the write pointer at the current end of the file. On POSIX systems, each write to a file opened with O_APPEND is atomic with respect to the pointer position: the OS moves the file pointer to the end and writes in a single atomic kernel operation, preventing interleaving when multiple processes append to the same file (the append is not atomically delivered as a unit to disk — just the seek-and-write is atomic). This makes append mode safe for multiple processes logging to the same file. On Windows, O_APPEND does not provide the same atomicity guarantee. FileDescriptor.out and FileDescriptor.err are the file descriptors for standard output and standard error, respectively. new FileOutputStream(FileDescriptor.out) creates an OutputStream connected to stdout. This is useful for writing binary data to stdout (for pipe-based programs) without using System.out, which is a PrintStream that might add its own buffering or formatting.
Java
// ── All constructor forms ─────────────────────────────────────────────
// Truncate existing, create if absent:
FileOutputStream fos1 = new FileOutputStream("/tmp/output.bin");

// Same, from File object:
File f = new File("/tmp/output.bin");
FileOutputStream fos2 = new FileOutputStream(f);

// Append mode — does NOT truncate existing file:
FileOutputStream fos3 = new FileOutputStream("/tmp/log.txt", true);
FileOutputStream fos4 = new FileOutputStream(f, true);

// From FileDescriptor — stdout:
FileOutputStream toStdout = new FileOutputStream(FileDescriptor.out);
toStdout.write("Binary to stdout
".getBytes(StandardCharsets.UTF_8));

// ── The truncation trap — file destroyed at construction ──────────────
File existingFile = new File("/tmp/important.dat");
// Before: existingFile contains 1MB of critical data

FileOutputStream dangerous = new FileOutputStream(existingFile);
// AFTER CONSTRUCTION: existingFile is now 0 bytes — data GONE even without any write!

// WRONG pattern: condition check BEFORE constructor doesn't help if file changes:
if (shouldWrite) {
    try (FileOutputStream fos = new FileOutputStream("output.bin")) {
        // Safe if shouldWrite is true — but what if shouldWrite was incorrectly evaluated?
    }
}

// SAFE pattern: construct only when committed to writing:
Path target = Path.of("/tmp/output.bin");
if (Files.exists(target) && !okToOverwrite()) {
    throw new IOException("Would overwrite " + target + " without permission");
}
try (FileOutputStream fos = new FileOutputStream(target.toFile())) {
    writeData(fos);
}

// ── Append mode — log file example ────────────────────────────────────
// Multiple opens in append mode — all writes are safe:
for (int run = 0; run < 3; run++) {
    try (PrintWriter log = new PrintWriter(new BufferedWriter(
            new OutputStreamWriter(
                new FileOutputStream("/tmp/app.log", true),  // true = append
                StandardCharsets.UTF_8)))) {
        log.printf("[Run %d] %s: Processing complete%n",
            run, LocalDateTime.now());
    }
}
// File contains all three runs — no truncation

// ── NIO.2 OpenOptions — more explicit than append boolean ─────────────
// Equivalent to append=false (create or truncate):
try (OutputStream os = Files.newOutputStream(Path.of("/tmp/output.bin"),
        StandardOpenOption.CREATE,
        StandardOpenOption.TRUNCATE_EXISTING,
        StandardOpenOption.WRITE)) {
    os.write(data);
}

// Equivalent to append=true (create or append):
try (OutputStream os = Files.newOutputStream(Path.of("/tmp/log.bin"),
        StandardOpenOption.CREATE,
        StandardOpenOption.APPEND,
        StandardOpenOption.WRITE)) {
    os.write(logEntry);
}

// Create new file, fail if exists (no equivalent in FileOutputStream):
try (OutputStream os = Files.newOutputStream(Path.of("/tmp/new.bin"),
        StandardOpenOption.CREATE_NEW,   // throws FileAlreadyExistsException if exists
        StandardOpenOption.WRITE)) {
    os.write(data);
}

// ── FileDescriptor — flushing to hardware ────────────────────────────
try (FileOutputStream fos = new FileOutputStream("/tmp/critical.bin")) {
    fos.write(criticalData);
    fos.flush();              // flush Java buffers to OS kernel buffer
    fos.getFD().sync();       // fsync: flush OS kernel buffer to disk hardware
    // After sync(): data is on disk even if OS crashes or power fails
    System.out.println("Data safely on disk");
}

Buffering, DataOutputStream, and Writing Structured Data

FileOutputStream provides no internal buffering. Every write(int b) call results in a system call; every write(byte[] b) call results in one system call for the entire array; write(byte[] b, int off, int len) results in one system call for len bytes. For the single-byte write, the system call overhead (100–500 nanoseconds) dominates the actual work, making unbuffered byte-at-a-time writing catastrophically slow. For array-based writes, the system call overhead is amortized across the bytes in the array, but the OS may break large writes into multiple kernel operations internally. BufferedOutputStream wraps FileOutputStream and maintains an internal byte array (default 8192 bytes, configurable via the constructor). write(int b) writes to the buffer array without a system call; the buffer is flushed to the underlying FileOutputStream (triggering a system call) only when it fills, when flush() is called explicitly, or when close() is called. This dramatically reduces system call count: writing 1MB in single bytes produces 1,048,576 system calls without buffering but only ~128 system calls with 8KB buffering. The flush obligation is the critical correctness issue with BufferedOutputStream. If the JVM terminates without close() being called, and close() is not called because an exception bypassed it, the contents of the unflushed buffer are lost permanently — not written to the file and not reported as an error. This is the data loss scenario that try-with-resources prevents: the close() method of the outermost stream in the try-with-resources declaration is always called, which for BufferedOutputStream calls flush() before closing the underlying FileOutputStream. DataOutputStream wraps any OutputStream and adds methods for writing every Java primitive type in a defined big-endian binary format: writeInt(int v) writes exactly 4 bytes, writeLong(long v) writes exactly 8 bytes, writeDouble(double v) writes exactly 8 bytes, writeUTF(String s) writes a 2-byte length prefix followed by the string in modified UTF-8. The format is defined precisely in the DataOutput interface and documented to be big-endian and Java-specific. DataOutputStream also tracks the total number of bytes written via size(), which is useful for length-prefixed binary formats where you need to write the payload length before the payload. For writing structured data that will be read on multiple platforms or by non-Java code, DataOutputStream is usually wrong: big-endian Java-specific encoding is rarely what external formats require. Use NIO's ByteBuffer with explicit byte order for interoperable binary writing, or a proper serialization format (Protocol Buffers, MessagePack, etc.).
Java
// ── Buffered writing — always wrap FileOutputStream ──────────────────
// Unbuffered: 1,000,000 system calls for 1MB:
long start = System.nanoTime();
try (FileOutputStream fos = new FileOutputStream("/tmp/unbuffered.bin")) {
    for (int i = 0; i < 1_000_000; i++) fos.write(i & 0xFF);
}
System.out.printf("Unbuffered: %.0f ms%n", (System.nanoTime()-start)/1e6); // ~3000ms

// Buffered: ~128 system calls for 1MB (8KB buffer default):
start = System.nanoTime();
try (BufferedOutputStream bos = new BufferedOutputStream(
        new FileOutputStream("/tmp/buffered.bin"))) {
    for (int i = 0; i < 1_000_000; i++) bos.write(i & 0xFF);
}
System.out.printf("Buffered: %.0f ms%n", (System.nanoTime()-start)/1e6); // ~10ms

// Large custom buffer for bulk I/O:
try (BufferedOutputStream bos = new BufferedOutputStream(
        new FileOutputStream("/tmp/large.bin"), 65536)) {  // 64KB buffer
    // Each 64KB fill triggers 1 system call
}

// ── DataOutputStream — typed binary writing ───────────────────────────
// Writing a binary record format:
try (DataOutputStream dos = new DataOutputStream(
        new BufferedOutputStream(new FileOutputStream("/tmp/records.bin")))) {

    // Write file header:
    dos.writeInt(0xDEADBEEF);          // magic number (4 bytes)
    dos.writeShort(2);                 // file format version (2 bytes)
    dos.writeByte(0);                  // reserved (1 byte)
    dos.writeByte(0);                  // reserved (1 byte)
    dos.writeInt(3);                   // record count (4 bytes, header so far: 12 bytes)
    dos.writeInt(0);                   // placeholder for checksum (updated later)

    // Write 3 records:
    String[] names = {"Alice", "Bob", "Carol"};
    double[] scores = {98.5, 87.3, 92.1};

    for (int i = 0; i < 3; i++) {
        dos.writeInt(i + 1);           // ID (4 bytes)
        dos.writeUTF(names[i]);        // length-prefixed modified UTF-8
        dos.writeDouble(scores[i]);    // IEEE 754 double (8 bytes)
        dos.writeLong(System.currentTimeMillis());  // timestamp (8 bytes)
    }

    System.out.println("Total bytes written: " + dos.size());
}

// Reading back the same format:
try (DataInputStream dis = new DataInputStream(
        new BufferedInputStream(new FileInputStream("/tmp/records.bin")))) {
    int magic   = dis.readInt();
    int version = dis.readShort() & 0xFFFF;
    dis.readByte(); dis.readByte();    // reserved
    int count   = dis.readInt();
    int checksum = dis.readInt();
    System.out.printf("Magic: 0x%X  Version: %d  Records: %d%n", magic, version, count);

    for (int i = 0; i < count; i++) {
        int id        = dis.readInt();
        String name   = dis.readUTF();
        double score  = dis.readDouble();
        long timestamp = dis.readLong();
        System.out.printf("  ID=%d Name=%s Score=%.1f%n", id, name, score);
    }
}

// ── ByteBuffer for portable binary writing (non-Java interop) ─────────
// For formats that need little-endian or specific byte layouts:
try (FileOutputStream fos = new FileOutputStream("/tmp/portable.bin");
     FileChannel channel = fos.getChannel()) {

    ByteBuffer buf = ByteBuffer.allocate(1024).order(ByteOrder.LITTLE_ENDIAN);
    buf.putInt(0x00534550);             // "PES" in little-endian (example format)
    buf.putShort((short) 0x014C);      // machine type: Intel 386
    buf.putShort((short) 3);           // number of sections
    buf.putInt((int)(System.currentTimeMillis() / 1000));  // timestamp
    buf.flip();
    channel.write(buf);
}

flush(), fsync(), Atomic Writes, and NIO.2 Alternatives

Java's flush() method and the OS concept of fsync() address different levels of the write durability hierarchy. A write to a BufferedOutputStream goes to Java's in-process buffer. flush() writes the Java buffer to the OS kernel buffer (calls the OS write() system call). fsync() (accessible via FileDescriptor.sync() or FileChannel.force(true)) instructs the OS kernel to flush its buffer to the physical storage device — spinning disk, SSD, or NVM. After flush(): data is safe from JVM crash but not from OS crash or power failure. After fsync(): data is safe from hardware failure at the OS level (with hardware-level write barriers satisfied). For most application data, flush() is sufficient: the OS kernel reliably writes dirty pages to disk in the background within seconds, and OS crashes are rare. For write-ahead logs, transaction commit records, checksums, or any data where durability in the face of power failure is required, fsync() is necessary. Database write paths, financial transaction logs, and crash-recovery files all require fsync(). The atomic write pattern — write-to-temp-then-rename — is the correct approach for crash-safe file updates. The problem without it: writing new data to an existing file involves overwriting bytes in-place. If the JVM or OS crashes mid-write, the file is left in a partially-written state, with some old bytes and some new bytes, and possibly inconsistent. The solution: write the new data to a temporary file in the same directory, fsync the temp file, then atomically rename the temp file to the target. On POSIX systems, rename() is atomic at the kernel level: the directory entry either points to the old inode or the new inode, never to a partially-updated entry. This is the basis of crash-safe file updates in databases, configuration management, and package managers. NIO.2 provides cleaner APIs for common FileOutputStream patterns. Files.write(Path, byte[]) writes a byte array to a file in a single call with no resource management. Files.writeString(Path, CharSequence) writes a string. Files.newOutputStream(Path, OpenOption...) returns an OutputStream with explicit open options including CREATE_NEW (fail if exists), APPEND, TRUNCATE_EXISTING, SYNC (sync each write to hardware), and DSYNC (sync data but not metadata). The SYNC option is equivalent to opening the file with O_SYNC on POSIX: every write call synchronously flushes to hardware, which eliminates the need for explicit fsync() at the cost of write throughput.
Java
// ── flush() vs sync() — the durability hierarchy ─────────────────────
try (FileOutputStream fos = new FileOutputStream("/tmp/durable.bin")) {
    try (BufferedOutputStream bos = new BufferedOutputStream(fos)) {
        bos.write(importantData);

        // Level 1: flush Java buffer → OS kernel buffer
        bos.flush();
        // After flush(): safe from JVM crash; NOT safe from OS crash/power failure

        // Level 2: OS kernel buffer → storage hardware
        fos.getFD().sync();   // equivalent to fsync() system call
        // After sync(): safe from OS crash and power failure
        // (assuming hardware write cache is acknowledged — many SSDs don't honor this
        //  without additional drive-level flush commands)
    }
}

// FileChannel.force() — the NIO equivalent of fsync:
try (FileOutputStream fos = new FileOutputStream("/tmp/durable.bin");
     FileChannel channel = fos.getChannel()) {
    channel.write(ByteBuffer.wrap(importantData));
    channel.force(true);    // true = also sync file metadata (size, timestamps)
                            // false = only sync data, not metadata (faster)
}

// ── Atomic write-to-temp-then-rename pattern ──────────────────────────
void atomicWrite(Path targetFile, byte[] newContent) throws IOException {
    // Create temp file in SAME directory as target (required for atomic rename):
    Path tempFile = Files.createTempFile(
        targetFile.getParent(),   // same directory — same filesystem!
        ".tmp-",                  // prefix
        null                      // suffix (null = ".tmp")
    );

    try {
        // Write new content to temp file:
        try (FileOutputStream fos = new FileOutputStream(tempFile.toFile());
             BufferedOutputStream bos = new BufferedOutputStream(fos)) {
            bos.write(newContent);
            bos.flush();       // flush to OS kernel buffer
            fos.getFD().sync(); // flush to storage hardware
        }

        // Atomically replace target with temp:
        Files.move(tempFile, targetFile,
            StandardCopyOption.ATOMIC_MOVE,      // kernel-level atomic rename
            StandardCopyOption.REPLACE_EXISTING  // overwrite if target exists
        );
        // At this point: target file either has old content or new content
        // There is NO window where it could be partially updated or missing

    } catch (Exception e) {
        Files.deleteIfExists(tempFile);   // clean up temp on failure
        throw e;
    }
}

// Usage:
atomicWrite(Path.of("/etc/myapp/config.json"), newConfigBytes);

// ── NIO.2 convenience methods — simpler than FileOutputStream chains ──
// Write byte array to file (create or overwrite):
Files.write(Path.of("/tmp/data.bin"), dataBytes);

// Write with explicit options:
Files.write(
    Path.of("/tmp/data.bin"),
    dataBytes,
    StandardOpenOption.CREATE,
    StandardOpenOption.TRUNCATE_EXISTING,
    StandardOpenOption.WRITE
);

// Write string to file:
Files.writeString(Path.of("/tmp/result.txt"), "Hello, World!
",
    StandardCharsets.UTF_8,
    StandardOpenOption.CREATE,
    StandardOpenOption.TRUNCATE_EXISTING
);

// Append string to file:
Files.writeString(Path.of("/tmp/log.txt"), logEntry + "
",
    StandardCharsets.UTF_8,
    StandardOpenOption.CREATE,
    StandardOpenOption.APPEND
);

// Write lines (adds system line separator after each):
Files.write(Path.of("/tmp/lines.txt"),
    List.of("line 1", "line 2", "line 3"),
    StandardCharsets.UTF_8
);

// OutputStream with SYNC — every write goes to hardware:
try (OutputStream os = Files.newOutputStream(Path.of("/tmp/sync.bin"),
        StandardOpenOption.CREATE,
        StandardOpenOption.WRITE,
        StandardOpenOption.SYNC)) {   // O_SYNC: each write() flushes to hardware
    os.write(criticalData);   // write + hardware sync in one system call
    // No explicit sync() needed — SYNC option handles it
}

// ── Complete practical example: binary file writer with error handling ─
void writeBinaryDatabase(Path file, List<Record> records) throws IOException {
    Path temp = Files.createTempFile(file.getParent(), ".db-tmp-", null);
    try {
        try (DataOutputStream dos = new DataOutputStream(
                new BufferedOutputStream(new FileOutputStream(temp.toFile()), 65536))) {

            // Header:
            dos.writeInt(0x4D594442);      // "MYDB" magic
            dos.writeInt(1);               // version
            dos.writeInt(records.size());  // count
            dos.writeLong(System.currentTimeMillis()); // creation time

            // Records:
            for (Record r : records) {
                dos.writeInt(r.id());
                dos.writeUTF(r.name());
                dos.writeDouble(r.value());
            }

            dos.flush();   // flush Java buffer to OS
            // get underlying FileOutputStream for sync:
        }

        // After DataOutputStream.close(), underlying FileOutputStream is also closed.
        // For explicit fsync before rename, use separate try block with FileChannel:
        try (FileChannel ch = FileChannel.open(temp, StandardOpenOption.WRITE)) {
            ch.force(true);   // fsync
        }

        // Atomic rename to final location:
        Files.move(temp, file,
            StandardCopyOption.ATOMIC_MOVE,
            StandardCopyOption.REPLACE_EXISTING);

    } catch (IOException e) {
        Files.deleteIfExists(temp);
        throw new IOException("Failed to write database to " + file, e);
    }
}

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.