☕ Java

DataOutputStream

DataOutputStream wraps any OutputStream and adds methods for writing Java primitive types in a machine-independent binary format using big-endian byte order. It writes boolean, byte, short, int, long, float, double, and char values using fixed byte widths, and strings using a modified UTF-8 encoding with a 2-byte length prefix. Every value written by DataOutputStream can be read back by DataInputStream using the corresponding read method, making them the standard pair for binary serialization of primitive data across files, sockets, pipes, and inter-process communication. DataOutputStream is unbuffered — each write call propagates to the underlying OutputStream — so it should always be wrapped over a BufferedOutputStream for file or network I/O. DataOutputStream tracks a written byte counter via the size() method, which returns the total bytes written since construction (wraps at Integer.MAX_VALUE). This entry covers all write methods and their byte-level encoding, size() semantics, the writeUTF contract and its limitations, flush() behavior, composition with BufferedOutputStream, and patterns for building binary protocol messages.

Construction, Write Methods, and Byte Encoding

DataOutputStream is constructed with new DataOutputStream(OutputStream out). Like DataInputStream, it adds no buffer — it should always be layered over a BufferedOutputStream: new DataOutputStream(new BufferedOutputStream(new FileOutputStream(path))). The write methods encode values as fixed-size big-endian byte sequences and pass them to the underlying OutputStream. writeBoolean(boolean v) writes 1 byte: 1 for true, 0 for false. writeByte(int v) writes the low-order byte of the int (only the lowest 8 bits are written). writeShort(int v) writes the low-order 16 bits as a 2-byte big-endian short. writeChar(int v) writes the low-order 16 bits as a 2-byte big-endian character. writeInt(int v) writes a 4-byte big-endian integer. writeLong(long v) writes an 8-byte big-endian long. writeFloat(float v) converts to an int bit pattern via Float.floatToIntBits(v) and writes 4 bytes. writeDouble(double v) converts to a long bit pattern via Double.doubleToLongBits(v) and writes 8 bytes. write(byte[] b, int off, int len) writes len bytes from the array starting at off — inherited from FilterOutputStream, passes directly to the underlying OutputStream. The big-endian encoding is fixed and cannot be changed. For writing little-endian data (required by many binary file formats including BMP, WAV, AVI, most Windows binary formats, and ELF), use java.nio.ByteBuffer with order(ByteOrder.LITTLE_ENDIAN) to encode values, then write the backing array. The ByteBuffer approach also allows batch encoding — accumulate multiple fields in a buffer and write them all at once, reducing write() call overhead. writeBytes(String s) writes only the low-order byte of each character — this is a legacy ASCII-only method that silently discards the high byte of Unicode characters. It should never be used for non-ASCII strings. Use writeUTF() for strings in the DataOutputStream format, or encode the string manually with String.getBytes(charset) and write the byte array for standard encoding.
Java
// ── Construction: always wrap with BufferedOutputStream ───────────────
try (DataOutputStream dos = new DataOutputStream(
        new BufferedOutputStream(new FileOutputStream("data.bin")))) {
    // writes accumulate in 8192-byte buffer
}

// ── Writing all primitive types ───────────────────────────────────────
try (DataOutputStream dos = new DataOutputStream(
        new BufferedOutputStream(new FileOutputStream("primitives.bin")))) {

    dos.writeBoolean(true);       // 1 byte: value 1
    dos.writeBoolean(false);      // 1 byte: value 0
    dos.writeByte(127);           // 1 byte: 0x7F (low-order 8 bits of int)
    dos.writeByte(-1);            // 1 byte: 0xFF
    dos.writeShort(1000);         // 2 bytes big-endian: 0x03 0xE8
    dos.writeShort(-1);           // 2 bytes: 0xFF 0xFF
    dos.writeChar('A');           // 2 bytes big-endian: 0x00 0x41
    dos.writeChar('€');           // 2 bytes big-endian: 0x20 0xAC (U+20AC)
    dos.writeInt(0x12345678);     // 4 bytes: 0x12 0x34 0x56 0x78
    dos.writeInt(-1);             // 4 bytes: 0xFF 0xFF 0xFF 0xFF
    dos.writeLong(Long.MAX_VALUE);// 8 bytes: 0x7F 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF
    dos.writeFloat(3.14f);        // 4 bytes IEEE 754: Float.floatToIntBits(3.14f)
    dos.writeDouble(Math.PI);     // 8 bytes IEEE 754: Double.doubleToLongBits(π)

    System.out.println("Written: " + dos.size() + " bytes");  // total written so far
}

// ── Byte layout of writeInt(0x12345678) ──────────────────────────────
// Byte 0 (MSB): 0x12
// Byte 1:       0x34
// Byte 2:       0x56
// Byte 3 (LSB): 0x78
// Big-endian: most significant byte written first (network byte order)

// ── Little-endian via ByteBuffer ─────────────────────────────────────
// For BMP, WAV, AVI, Windows PE, ELF, and other little-endian formats:
try (DataOutputStream dos = new DataOutputStream(
        new BufferedOutputStream(new FileOutputStream("le_data.bin")))) {

    // Encode int 0x12345678 in little-endian:
    ByteBuffer bb = ByteBuffer.allocate(4).order(ByteOrder.LITTLE_ENDIAN);
    bb.putInt(0x12345678);
    dos.write(bb.array());   // writes: 0x78 0x56 0x34 0x12 (little-endian)

    // Batch encoding: encode multiple fields at once
    ByteBuffer multi = ByteBuffer.allocate(16).order(ByteOrder.LITTLE_ENDIAN);
    multi.putInt(42);
    multi.putLong(System.currentTimeMillis());
    multi.putInt(100);
    dos.write(multi.array());
}

// ── writeBytes: ASCII-only legacy method — avoid for Unicode ──────────
dos.writeBytes("ASCII only");   // LOW BYTE ONLY — truncates Unicode silently
// 'é' (U+00E9) → writes 0xE9 only (one byte) — data loss for high bytes > 0x7F
// NEVER use writeBytes for Unicode content

// For UTF-8 string writing with explicit length:
byte[] utf8Bytes = "Hello, 世界!".getBytes(StandardCharsets.UTF_8);
dos.writeInt(utf8Bytes.length);   // 4-byte length prefix
dos.write(utf8Bytes);             // followed by UTF-8 bytes

writeUTF(), size(), flush(), and the DataOutputStream/DataInputStream Contract

writeUTF(String s) encodes the string in DataOutputStream's modified UTF-8 format: first a 2-byte big-endian unsigned short containing the number of bytes in the encoded representation (not the number of characters), followed by that many bytes of modified UTF-8 text. The maximum string size is 65535 bytes in modified UTF-8 encoding — strings longer than this cause UTFDataFormatException. For the BMP characters (U+0000 to U+FFFF), modified UTF-8 differs from standard UTF-8 only for the null character (U+0000, encoded as 0xC0 0x80 in modified UTF-8 vs 0x00 in standard UTF-8). For supplementary characters (U+10000 and above), modified UTF-8 encodes each as a 6-byte sequence (via surrogate pairs) rather than the 4-byte standard UTF-8 encoding. The writeUTF/readUTF pair is intended for internal Java serialization — it is used by Java object serialization, by the class file format (for constant pool strings), and by DataInputStream/DataOutputStream when a text string must be embedded in a binary stream without a separate length field. It is not suitable for writing files intended to be read by non-Java programs, because the format is not standard UTF-8. size() returns the total number of bytes written to the DataOutputStream since it was constructed, as an int. The count wraps around if more than Integer.MAX_VALUE bytes are written (approximately 2GB). size() is useful for: checking how large a serialized object is before deciding whether to buffer or stream it, computing message offsets for protocol framing, and verifying that a write sequence produces the expected byte count. flush() propagates to the underlying OutputStream. For DataOutputStream over BufferedOutputStream, flush() causes the BufferedOutputStream to write its buffer to the underlying FileOutputStream (or socket OutputStream). It does not guarantee disk durability — for that, call the underlying FileOutputStream.getFD().sync() or use NIO FileChannel.force(). The close() method calls flush() and then closes the underlying OutputStream.
Java
// ── writeUTF: DataOutputStream's modified UTF-8 format ────────────────
try (DataOutputStream dos = new DataOutputStream(
        new BufferedOutputStream(new FileOutputStream("strings.bin")));
     DataInputStream dis = new DataInputStream(
        new BufferedInputStream(new FileInputStream("strings.bin")))) {

    // Write:
    dos.writeUTF("Hello");          // 2-byte length (5) + 5 bytes
    dos.writeUTF("世界");           // 2-byte length (6) + 6 bytes (3 bytes per CJK char)
    dos.writeUTF("Hello, 世界!");   // 2-byte length + encoded bytes
    dos.flush();                    // ensure all bytes reach the file

    // Read back — exactly matches what was written:
    System.out.println(dis.readUTF()); // "Hello"
    System.out.println(dis.readUTF()); // "世界"
    System.out.println(dis.readUTF()); // "Hello, 世界!"
}

// ── writeUTF limitations ─────────────────────────────────────────────
// Max 65535 bytes in modified UTF-8 (NOT max 65535 characters):
String tooLong = "A".repeat(65536);  // 65536 ASCII chars = 65536 bytes
try {
    dos.writeUTF(tooLong);  // throws UTFDataFormatException: string too long
} catch (UTFDataFormatException e) {
    System.out.println("String too long for writeUTF: " + e.getMessage());
}

// For long strings: write 4-byte length + raw UTF-8 bytes
byte[] utf8 = tooLong.getBytes(StandardCharsets.UTF_8);
dos.writeInt(utf8.length);   // 4-byte length — handles strings up to 2GB
dos.write(utf8);             // raw UTF-8 bytes (standard format)

// ── size(): total bytes written since construction ────────────────────
ByteArrayOutputStream baos = new ByteArrayOutputStream();
DataOutputStream counter = new DataOutputStream(baos);

counter.writeInt(42);         // 4 bytes
counter.writeDouble(3.14);    // 8 bytes
counter.writeUTF("Hi");       // 2 (length) + 2 (bytes) = 4 bytes

System.out.println("Written: " + counter.size() + " bytes");  // 16 bytes
System.out.println("Buffer size: " + baos.size() + " bytes"); // also 16 bytes

// size() counts ALL bytes, including multi-byte writes:
ByteArrayOutputStream baos2 = new ByteArrayOutputStream();
DataOutputStream dos2 = new DataOutputStream(baos2);
dos2.writeLong(0L);      // 8 bytes
dos2.writeLong(0L);      // 8 bytes more
dos2.write(new byte[100]); // 100 bytes more
System.out.println(dos2.size());  // 116

// ── flush() propagation chain ─────────────────────────────────────────
try (DataOutputStream dos3 = new DataOutputStream(
        new BufferedOutputStream(
            new FileOutputStream("flushed.bin")))) {

    dos3.writeInt(42);
    dos3.writeLong(123456789L);
    // At this point: bytes in BufferedOutputStream's internal buffer (not on disk)

    dos3.flush();
    // After flush: BufferedOutputStream.flush() writes buffer to FileOutputStream
    // FileOutputStream makes OS system call — bytes in OS kernel buffer
    // NOT guaranteed on disk yet (OS may still buffer)

    // For disk durability:
    // ((FileOutputStream)((BufferedOutputStream)dos3.out).out).getFD().sync();
    // (requires accessing the FileDescriptor through the chain)
}

Binary Protocol Serialization and Message Building Patterns

DataOutputStream is the standard tool for building binary protocol messages in Java. The typical pattern for a framed binary protocol: write the message body to a ByteArrayOutputStream wrapped in DataOutputStream (so size() gives the exact message length), then write the frame header (type + length) to the real output stream, then write the body bytes. This two-phase approach avoids the problem of not knowing the message length before serializing the body. For high-performance binary serialization, the ByteBuffer approach competes with DataOutputStream. ByteBuffer.allocate() creates a buffer of fixed size; the put methods (putInt, putLong, putDouble) write values; flip() prepares the buffer for reading; and the buffer's array() or a channel write sends the bytes. ByteBuffer has advantages in: supporting both big-endian and little-endian byte orders, enabling zero-copy NIO I/O via DirectByteBuffer, and being processable by SIMD-aware JIT optimizations. DataOutputStream has advantages in: being a standard OutputStream chain component, supporting writeUTF() for quick string serialization, and being more readable for protocol implementations that map naturally to a sequence of typed field writes. A common pattern for writing variable-length fields: writeShort(byteArray.length) followed by write(byteArray) for fields up to 65535 bytes; writeInt(byteArray.length) followed by write(byteArray) for larger fields. This is more efficient than writeUTF() for arbitrary binary data because writeUTF() is text-specific (applies modified UTF-8 encoding). For binary data like cryptographic material, image data, or arbitrary byte sequences, always use explicit length + raw bytes.
Java
// ── Two-phase message building: serialize body first, then frame ───────
public static void sendMessage(DataOutputStream networkDos, int msgType, Object payload)
        throws IOException {

    // Phase 1: serialize payload to byte array to get its length
    ByteArrayOutputStream bodyBaos = new ByteArrayOutputStream(256);
    DataOutputStream bodyDos = new DataOutputStream(bodyBaos);

    if (payload instanceof LoginRequest lr) {
        bodyDos.writeInt(lr.userId());
        bodyDos.writeShort(lr.flags());
        byte[] nameBytes = lr.username().getBytes(StandardCharsets.UTF_8);
        bodyDos.writeShort(nameBytes.length);   // 2-byte length prefix
        bodyDos.write(nameBytes);               // UTF-8 bytes
    } else if (payload instanceof TradeRequest tr) {
        bodyDos.writeLong(tr.timestamp());
        bodyDos.writeInt(tr.symbolId());
        bodyDos.writeLong(Math.round(tr.price() * 10000));  // fixed-point
        bodyDos.writeInt(tr.quantity());
    }
    bodyDos.flush();
    byte[] body = bodyBaos.toByteArray();

    // Phase 2: write frame header + body to network
    networkDos.writeInt(msgType);   // 4-byte type
    networkDos.writeInt(body.length); // 4-byte length
    networkDos.write(body);           // body bytes
    networkDos.flush();               // ensure TCP sends the complete message
}

// ── ByteBuffer alternative for big-endian binary serialization ────────
public static byte[] serializeTrade(long timestamp, int symbolId,
        double price, int quantity) {
    ByteBuffer buf = ByteBuffer.allocate(24);  // 8+4+8+4 bytes
    buf.order(ByteOrder.BIG_ENDIAN);           // match DataOutputStream
    buf.putLong(timestamp);
    buf.putInt(symbolId);
    buf.putLong(Math.round(price * 10000));    // fixed-point
    buf.putInt(quantity);
    return buf.array();   // ready to write to any OutputStream
}

// ── Writing a complete binary file format ─────────────────────────────
// Simple custom binary format: [8-byte magic][4-byte version][4-byte record_count][records...]
// Each record: [8-byte id][4-byte value][2-byte tag_length][N-byte tag]
public static void writeDataFile(Path path, List<DataRecord> records) throws IOException {
    try (DataOutputStream dos = new DataOutputStream(
            new BufferedOutputStream(new FileOutputStream(path.toFile()), 65536))) {

        // File header:
        dos.writeLong(0x4A415641_44415441L);  // "JAVADATA" magic bytes
        dos.writeInt(1);                       // format version 1
        dos.writeInt(records.size());          // record count

        // Records:
        for (DataRecord rec : records) {
            dos.writeLong(rec.id());
            dos.writeInt(rec.value());

            byte[] tagBytes = rec.tag().getBytes(StandardCharsets.UTF_8);
            dos.writeShort(tagBytes.length);   // 2-byte tag length (max 65535 bytes)
            dos.write(tagBytes);
        }

        System.out.printf("Wrote %d records, %d total bytes%n",
            records.size(), dos.size());
    }
}

// ── Reading back the same format with DataInputStream ─────────────────
public static List<DataRecord> readDataFile(Path path) throws IOException {
    List<DataRecord> records = new ArrayList<>();
    try (DataInputStream dis = new DataInputStream(
            new BufferedInputStream(new FileInputStream(path.toFile()), 65536))) {

        // Verify magic:
        long magic = dis.readLong();
        if (magic != 0x4A415641_44415441L) throw new IOException("Not a JAVADATA file");

        int version = dis.readInt();
        if (version != 1) throw new IOException("Unsupported version: " + version);

        int count = dis.readInt();

        for (int i = 0; i < count; i++) {
            long id    = dis.readLong();
            int  value = dis.readInt();

            int tagLen    = dis.readUnsignedShort();
            byte[] tagBytes = new byte[tagLen];
            dis.readFully(tagBytes);
            String tag = new String(tagBytes, StandardCharsets.UTF_8);

            records.add(new DataRecord(id, value, tag));
        }
    }
    return records;
}

Related Topics in Java I/O

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