Skip to content
Java io 7 min read

DataOutputStream

Raw byte streams are great for moving opaque data around, but what happens when you want to write an int, a double, or a boolean directly to a file or socket? DataOutputStream handles exactly that — it wraps any OutputStream and gives you typed write methods for every Java primitive, plus a portable string encoding called UTF-8 modified form.

What DataOutputStream Does

DataOutputStream lives in java.io and extends FilterOutputStream. It adds a set of writeXxx() methods that serialize Java primitives into a well-defined binary format: big-endian byte order, platform-independent, and fully compatible with its counterpart DataInputStream.

The key point is portability: when you write an int with writeInt(42), the stream always emits exactly 4 bytes in big-endian order regardless of the hardware running your program. Any JVM on any platform can read them back with DataInputStream.readInt() and get 42.

Note: DataOutputStream is a byte stream, not a character stream. For human-readable text output, prefer BufferedWriter or PrintWriter.

Core Write Methods

MethodBytes writtenDescription
writeBoolean(boolean v)11 for true, 0 for false
writeByte(int v)1Low 8 bits of v
writeShort(int v)2Big-endian signed 16-bit
writeChar(int v)2Big-endian UTF-16 code unit
writeInt(int v)4Big-endian signed 32-bit
writeLong(long v)8Big-endian signed 64-bit
writeFloat(float v)4IEEE 754 float bits
writeDouble(double v)8IEEE 754 double bits
writeUTF(String s)2 + encoded lengthLength-prefixed modified UTF-8
writeBytes(String s)s.length()Low bytes only — ASCII safe
writeChars(String s)2 × s.length()Each char as 2-byte big-endian

Basic Usage

import java.io.DataOutputStream;
import java.io.FileOutputStream;
import java.io.IOException;

public class BasicDataWrite {
    public static void main(String[] args) throws IOException {
        try (DataOutputStream dos = new DataOutputStream(
                new FileOutputStream("data.bin"))) {

            dos.writeInt(42);
            dos.writeDouble(3.14159);
            dos.writeBoolean(true);
            dos.writeUTF("Hello, DataOutputStream!");
        }
        System.out.println("Data written successfully.");
    }
}

Output:

Data written successfully.

The file data.bin now contains 1 + 4 + 8 + (2 + 24) = 39 bytes of structured binary data. You cannot read it in a text editor, but DataInputStream can reconstruct every value exactly.

Reading Back What You Wrote

Always read in the same order you wrote. DataInputStream is the mirror image of DataOutputStream:

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;

public class WriteAndRead {
    public static void main(String[] args) throws IOException {
        // Write
        try (DataOutputStream dos = new DataOutputStream(
                new FileOutputStream("record.bin"))) {
            dos.writeInt(1001);
            dos.writeDouble(59.95);
            dos.writeUTF("Widget");
        }

        // Read back
        try (DataInputStream dis = new DataInputStream(
                new FileInputStream("record.bin"))) {
            int id       = dis.readInt();
            double price = dis.readDouble();
            String name  = dis.readUTF();
            System.out.printf("ID: %d, Price: %.2f, Name: %s%n", id, price, name);
        }
    }
}

Output:

ID: 1001, Price: 59.95, Name: Widget

Warning: Reading fields out of order produces garbage values or throws EOFException. If you write int then double, you must read int then double. This is the most common DataOutputStream mistake.

Wrapping BufferedOutputStream for Performance

By itself, DataOutputStream makes one underlying write() call per field. Wrap it with BufferedOutputStream to batch those calls into fewer OS operations:

import java.io.BufferedOutputStream;
import java.io.DataOutputStream;
import java.io.FileOutputStream;
import java.io.IOException;

public class BufferedDataWrite {
    public static void main(String[] args) throws IOException {
        // BufferedOutputStream is in the middle of the stack
        try (DataOutputStream dos = new DataOutputStream(
                new BufferedOutputStream(
                        new FileOutputStream("scores.bin")))) {

            int[] scores = {98, 87, 76, 95, 60};
            for (int score : scores) {
                dos.writeInt(score);
            }
        }
        System.out.println("Scores saved.");
    }
}

Output:

Scores saved.

The stream chain reads: DataOutputStreamBufferedOutputStreamFileOutputStream → file on disk. DataOutputStream formats the primitive, BufferedOutputStream batches the bytes, and FileOutputStream makes the final system call.

Tracking Total Bytes Written

DataOutputStream exposes a size() method that returns the total number of bytes written so far. It’s based on an internal written counter (an int, so it wraps at 2 GB):

import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.IOException;

public class SizeDemo {
    public static void main(String[] args) throws IOException {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        try (DataOutputStream dos = new DataOutputStream(baos)) {
            dos.writeInt(100);    // 4 bytes
            dos.writeLong(999L);  // 8 bytes
            dos.writeUTF("Java"); // 2 (length) + 4 (chars) = 6 bytes
            System.out.println("Bytes written: " + dos.size()); // 18
        }
    }
}

Output:

Bytes written: 18

Tip: size() is handy when you need to record an offset table — write placeholder values first, then come back and patch them using RandomAccessFile after you know the final sizes.

writeUTF vs writeBytes vs writeChars

All three write strings, but they behave very differently:

MethodEncodingNull bytesSafe for non-ASCIIRead back with
writeUTF(String)Modified UTF-8 + 2-byte length prefixModified (C0 80)YesreadUTF()
writeBytes(String)Low byte of each char onlyYesNo (high byte discarded)Manual byte reads
writeChars(String)Raw UTF-16 (2 bytes/char)YesYesreadChar() × n

For most use cases, writeUTF is what you want. It’s self-delimiting (includes the length), handles any Unicode character, and has a matching readUTF() on the input side.

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;

public class WriteUTFDemo {
    public static void main(String[] args) throws IOException {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();

        try (DataOutputStream dos = new DataOutputStream(baos)) {
            dos.writeUTF("Café"); // "Café"
        }

        try (DataInputStream dis = new DataInputStream(
                new ByteArrayInputStream(baos.toByteArray()))) {
            System.out.println(dis.readUTF()); // Café
        }
    }
}

Output:

Café

Note: writeUTF uses a modified UTF-8 that differs from standard UTF-8 in one way: the null character \u0000 is encoded as two bytes (0xC0 0x80) rather than one zero byte. This ensures no embedded nulls appear in the stream, which some C-based readers expect. It is not standard UTF-8.

Constructors

DataOutputStream has a single constructor:

DataOutputStream(OutputStream out)

Pass any OutputStream — a FileOutputStream, BufferedOutputStream, ByteArrayOutputStream, a socket’s output stream, and so on. The same decorator pattern used throughout java.io.

Practical Example: Saving a Simple Record File

Here is a more complete example that writes a list of student records to a binary file and reads them back:

import java.io.*;

public class StudentRecords {

    record Student(int id, String name, double gpa) {}

    public static void main(String[] args) throws IOException {
        Student[] students = {
            new Student(1, "Alice", 3.9),
            new Student(2, "Bob",   3.4),
            new Student(3, "Carol", 3.7)
        };

        // Write
        try (DataOutputStream dos = new DataOutputStream(
                new BufferedOutputStream(new FileOutputStream("students.bin")))) {
            dos.writeInt(students.length); // record count first
            for (Student s : students) {
                dos.writeInt(s.id());
                dos.writeUTF(s.name());
                dos.writeDouble(s.gpa());
            }
        }

        // Read
        try (DataInputStream dis = new DataInputStream(
                new BufferedInputStream(new FileInputStream("students.bin")))) {
            int count = dis.readInt();
            for (int i = 0; i < count; i++) {
                int    id   = dis.readInt();
                String name = dis.readUTF();
                double gpa  = dis.readDouble();
                System.out.printf("ID=%d  Name=%-8s  GPA=%.1f%n", id, name, gpa);
            }
        }
    }
}

Output:

ID=1  Name=Alice     GPA=3.9
ID=2  Name=Bob       GPA=3.4
ID=3  Name=Carol     GPA=3.7

Writing the count first is a common pattern: the reader knows exactly how many records to expect without scanning for an end-of-file marker.

Under the Hood

When you call dos.writeInt(42), the method breaks the integer into four bytes and calls the underlying stream’s write(byte[], 0, 4) once. Here is the implementation in pseudocode:

writeInt(int v):
    buf[0] = (v >>> 24) & 0xFF  // most-significant byte first (big-endian)
    buf[1] = (v >>> 16) & 0xFF
    buf[2] = (v >>>  8) & 0xFF
    buf[3] = (v >>>  0) & 0xFF
    out.write(buf, 0, 4)
    incCount(4)

This big-endian format (network byte order) is compatible with Java’s DataInputStream, ObjectInputStream, and many binary protocols (e.g., Java class file format, some network protocols).

Thread safety: DataOutputStream inherits the synchronization behavior of FilterOutputStream. Individual method calls on DataOutputStream are not atomically thread-safe at a higher level. Concurrent writes from multiple threads can interleave bytes. Guard shared instances with synchronized blocks or give each thread its own stream.

Relationship to ObjectOutputStream: If you need to write full objects rather than individual primitives, use Serialization via ObjectOutputStream. Internally, ObjectOutputStream uses the same big-endian primitive encoding as DataOutputStream for primitive fields — they are close relatives in the java.io family tree.

DataOutputStream vs PrintStream

FeatureDataOutputStreamPrintStream
Output formatBinary (machine-readable)Text (human-readable)
Primitive supportTyped binary writeConverts to String via print()
Round-trip with DataInputStreamYes, losslessNo
EncodingBig-endian binaryPlatform or explicit charset
Best forBinary protocols, file formatsLogging, debugging, text output
Last updated June 13, 2026
Was this helpful?