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
// ── 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
// ── 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
// ── 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);
}
}