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.
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
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.
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.
| Aspect | Text output through PrintWriter | Binary output through DataOutputStream |
|---|---|---|
| Value 1234 | The characters 1, 2, 3, 4 | One fixed-size int value |
| Size as the number grows | Adds a character for each extra digit | Stays the same size for an int |
| Intended reader | A person reading the screen or file | Another program, or the same program later |
| Recovery of exact number | Requires parsing the characters back | Read 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.
OutputStream byteSink = new FileOutputStream("data.bin");
DataOutputStream dataSink = new DataOutputStream(byteSink);
dataSink.writeInt(258);
dataSink.writeDouble(3.5);
dataSink.close();- 1Line 1 creates an ordinary byte stream that writes to a file named
data.bin. The file name is only an example. - 2Line 2 wraps that stream in a
DataOutputStream. From this point the program sends values throughdataSink, and the bytes still end up inbyteSink. - 3Line 3 calls
writeIntwith the value 258. The wrapper converts the int into its four-byte binary form and passes those bytes tobyteSink. - 4Line 4 calls
writeDoublewith 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. - 5Line 5 closes the wrapper, which also closes the underlying stream. The file is now complete.
byteSinkis an ordinaryOutputStream; it knows only how to output bytes- The wrapper is created around
byteSinkand nameddataSink writeIntappends four bytes representing 258 to the streamwriteDoubleappends 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.
InputStream byteSource = new FileInputStream("data.bin");
DataInputStream dataSource = new DataInputStream(byteSource);
int n = dataSource.readInt();
double d = dataSource.readDouble();
dataSource.close();- 1Line 1 opens the byte stream on the file the writer produced.
- 2Line 2 wraps it in a
DataInputStream, which supplies the typed read methods. - 3Line 3 calls
readInt. The wrapper takes the first four bytes, interprets them in the format, and returns the int 258. - 4Line 4 calls
readDouble. The next eight bytes are interpreted as a double, returning 3.5. - 5Line 5 closes the wrapper and the stream beneath it.
byteSourceis an ordinaryInputStreamreading the same filereadIntconsumes the first four bytes and returns an intreadDoublethen consumes the next eight bytes and returns a double- The reads must happen in the same order the values were written
n is 258 and d is 3.5The 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.
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.
Mistakes Beginners Make
Common mistake Using PrintWriter output when a program must read the numbers back exactly
What it looks likeWriting 1234 as text and later parsing the characters to recover the number
Why it failsText output is arranged for a human reader; the binary format is arranged for a machine that must recover the original values
Do this insteadUse 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 likeWriting an int and then a double, but calling readDouble first
Why it failsThe format stores no type labels, so the reader interprets the bytes using the wrong pattern and gets incorrect values
Do this insteadCall 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 likeCreating a DataOutputStream with no underlying stream and expecting it to write to a file
Why it failsA
DataOutputStreamis a layer on top of an existing stream and does not create its own destinationDo this insteadWrap 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 likeTrying to wrap an OutputStream in a DataInputStream
Why it failsDataInputStreamwraps anInputStream, the input side of the pairDo this insteadWrap an InputStream in a DataInputStream when reading back data written by a DataOutputStream
Practice: Tracing a Write and Read Pair
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
readIntwould consume if the double's bytes come before it
Key Takeaways
DataOutputStreamwraps anyOutputStreamand adds typed methods such aswriteIntandwriteDoublethat write values in the internal binary format, which suits data meant for machines rather than peopleDataInputStreamwraps anyInputStreamand reads values back with methods such asreadIntandreadDouble, 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
DataOutputStreamis guaranteed to be readable by aDataInputStreameven across different types of computers, which is a major part of Java's platform independence