☕ 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

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.