Concepts / Byte Streams with OutputStream and InputStream

Byte Streams with OutputStream and InputStream

DataOutputStream writes typed values such as int and double into a stream in internal, binary-number format, and it is used when the output is meant for machines rather than people.

  • Programming

Why Machines Need Binary Output

Most of the output you have written so far probably looked like text. When you send data through a PrintWriter, each value is turned into the sequence of characters a person would read on a screen. That is exactly what you want when a human is the audience. It is less useful in two other situations: when the audience is another program, or when the same program will read the data back later and must recover the original numbers exactly. In those cases the output should be arranged for a machine rather than for a reader.

The java.io package addresses this need with a pair of byte-oriented classes. DataOutputStream writes data values into a stream using the internal, binary-number format, and DataInputStream reads such values back. This article follows a single value through that process so you can see exactly what happens at each step.

What do you think happens?

How many characters does the text form of the number 1234 contain when a PrintWriter writes it?

Reveal answer
Answer:

Four characters: the digits 1, 2, 3, and 4.

Text output stores the number as the characters a reader would see, so the text form grows with the number of digits. The binary form discussed in this article is a different representation of the same value.

Tracing What writeInt Puts into the Stream

A plain OutputStream can only output bytes. If your program holds an int, you would have to work out for yourself how to turn that value into bytes before sending it. DataOutputStream closes that gap. When you call writeInt, the wrapper converts the int into the internal binary format and hands the resulting bytes to the underlying stream. The program never sees those bytes directly; it only sees the typed method.

An int in Java occupies four bytes, so writeInt(258) appends exactly four bytes to the stream. The value 258 is 256 plus 2, which in four-byte binary is 0, 0, 1, 2 when written byte by byte from the most significant byte to the least. The diagram below shows the four byte positions and the value each one holds. Notice that the number is split across positions rather than stored as a sequence of digit characters.

Byte 00Byte 10Byte 21Byte 32
After writeInt(258) runs, which byte positions hold which parts of the value, and in what order does the format place them?

A double is handled the same way in principle. The double value is turned into its binary representation and appended as a fixed-size group of bytes, eight bytes for a Java double. Whatever the size, the important point is that the format is fixed in advance, so the layout does not depend on the program's text or on what the reader expects to see.

AspectText output through PrintWriterBinary output through DataOutputStream
Value 1234The characters 1, 2, 3, 4One fixed-size int value
Size as the number growsAdds a character for each extra digitStays the same size for an int
Intended readerA person reading the screen or fileAnother program, or the same program later
Recovery of exact numberRequires parsing the characters backRead back directly with readInt

Text and binary representations of the same kind of value

Wrapping a Byte Stream

DataOutputStream does not create its own destination. It is a layer placed on top of an existing stream. You can wrap any OutputStream in a DataOutputStream and then call the higher level methods on the wrapper. The stream you started with still receives the bytes; the wrapper decides how typed values become those bytes. The relationship is the same one PrintWriter has to Writer: the underlying object moves data, and the wrapper adds methods that accept particular types.

Code walkthrough
OutputStream byteSink = new FileOutputStream("data.bin");
DataOutputStream dataSink = new DataOutputStream(byteSink);
dataSink.writeInt(258);
dataSink.writeDouble(3.5);
dataSink.close();
  1. 1
    Line 1 creates an ordinary byte stream that writes to a file named data.bin. The file name is only an example.
  2. 2
    Line 2 wraps that stream in a DataOutputStream. From this point the program sends values through dataSink, and the bytes still end up in byteSink.
  3. 3
    Line 3 calls writeInt with the value 258. The wrapper converts the int into its four-byte binary form and passes those bytes to byteSink.
  4. 4
    Line 4 calls writeDouble with 3.5. The wrapper appends the double's binary form directly after the int's bytes, so the file now holds the int followed by the double.
  5. 5
    Line 5 closes the wrapper, which also closes the underlying stream. The file is now complete.
  • byteSink is an ordinary OutputStream; it knows only how to output bytes
  • The wrapper is created around byteSink and named dataSink
  • writeInt appends four bytes representing 258 to the stream
  • writeDouble appends eight bytes representing 3.5 after the int bytes
  • Closing the wrapper closes the stream it was built on

Reading the Values Back in Order

DataInputStream is the matched partner for reading. You wrap an existing InputStream in a DataInputStream, and the wrapper then gives you methods that read values of various types from the byte stream. Its methods for binary data mirror the write methods: readInt matches writeInt, and readDouble matches writeDouble. Each write method has a corresponding read method, so a program that writes an int with writeInt recovers it with readInt.

Code walkthrough
InputStream byteSource = new FileInputStream("data.bin");
DataInputStream dataSource = new DataInputStream(byteSource);
int n = dataSource.readInt();
double d = dataSource.readDouble();
dataSource.close();
  1. 1
    Line 1 opens the byte stream on the file the writer produced.
  2. 2
    Line 2 wraps it in a DataInputStream, which supplies the typed read methods.
  3. 3
    Line 3 calls readInt. The wrapper takes the first four bytes, interprets them in the format, and returns the int 258.
  4. 4
    Line 4 calls readDouble. The next eight bytes are interpreted as a double, returning 3.5.
  5. 5
    Line 5 closes the wrapper and the stream beneath it.
  • byteSource is an ordinary InputStream reading the same file
  • readInt consumes the first four bytes and returns an int
  • readDouble then consumes the next eight bytes and returns a double
  • The reads must happen in the same order the values were written
Output
n is 258 and d is 3.5

The reader does not store any labels in the file. It knows that the first value is an int and the second is a double only because the program that reads the file calls readInt and then readDouble. The reading program must therefore agree with the writing program on the sequence of types. The sequence diagram below traces the calls from the writer through the byte stream and back to the reader.

Writing programDataOutputStreamShared byte streamDataInputStreamReading programwriteInt(258)writeDouble(3.5)int bytes, then double bytessame bytes in same orderreadInt() firstreadDouble() second
If the program writes an int and then a double, what must the reader call, in what order, and what goes wrong if the calls are swapped?

The Cross-Platform Guarantee

The two classes are designed as a matched pair. A DataOutputStream turns typed values into the internal binary format, and a DataInputStream turns that format back into typed values. Data written by a DataOutputStream is guaranteed to be readable by a DataInputStream, even across different types of computers. The source identifies this property as a major part of Java's platform independence.

This matters because a program may write binary data on one machine and read it on another. The bytes in the middle are the shared meeting point. The two wrappers only need to agree with each other about the format; neither needs to know how the other machine is built. The diagram traces that path from one machine to another.

Machine A programwrites typed valuesDataOutputStreamconverts to binary formatShared byte streamformat-defined bytesDataInputStreamconverts back to valuesMachine B programreads typed valuestyped valuesbinary formatsame bytestyped values
Why can a file written on one machine be read correctly on a different machine, and how does the fixed binary format make that happen?

Mistakes Beginners Make

  • Common mistake

    Using PrintWriter output when a program must read the numbers back exactly

    What it looks like

    Writing 1234 as text and later parsing the characters to recover the number

    Why it fails

    Text output is arranged for a human reader; the binary format is arranged for a machine that must recover the original values

    Do this instead

    Use DataOutputStream for data meant to be read by programs, and DataInputStream to read it back

  • Common mistake

    Reading values in a different order from the order they were written

    What it looks like

    Writing an int and then a double, but calling readDouble first

    Why it fails

    The format stores no type labels, so the reader interprets the bytes using the wrong pattern and gets incorrect values

    Do this instead

    Call the read methods in exactly the same sequence as the matching write methods

  • Common mistake

    Expecting the wrapper to create its own destination

    What it looks like

    Creating a DataOutputStream with no underlying stream and expecting it to write to a file

    Why it fails

    A DataOutputStream is a layer on top of an existing stream and does not create its own destination

    Do this instead

    Wrap an existing OutputStream, such as a file stream, so the wrapper has somewhere to send the bytes

  • Common mistake

    Mixing the stream types in the read path

    What it looks like

    Trying to wrap an OutputStream in a DataInputStream

    Why it fails

    DataInputStream wraps an InputStream, the input side of the pair

    Do this instead

    Wrap an InputStream in a DataInputStream when reading back data written by a DataOutputStream

Practice: Tracing a Write and Read Pair

MEDIUM

A program writes three values in this order using a DataOutputStream: writeInt(7), writeDouble(2.5), writeInt(9). Write the sequence of read calls a correct reader must make, and explain what happens if the reader calls readInt, readInt, readDouble instead.

Hints
  • Each read method matches one write method, so the reader must mirror the write order exactly
  • The format stores no type labels, so the reader must know the sequence in advance
  • Consider which bytes the second readInt would consume if the double's bytes come before it

Key Takeaways

  • DataOutputStream wraps any OutputStream and adds typed methods such as writeInt and writeDouble that write values in the internal binary format, which suits data meant for machines rather than people
  • DataInputStream wraps any InputStream and reads values back with methods such as readInt and readDouble, each matching a write method
  • The reader must call the read methods in the same order as the write methods, because the format stores no type labels
  • Data written by a DataOutputStream is guaranteed to be readable by a DataInputStream even across different types of computers, which is a major part of Java's platform independence