Network Communication with Object Streams
Serializing an object means encoding it as a sequence of primitive-type data values so it can be output as bytes or characters, and reading it back reconstitutes a copy of the original object.
Why Saving a Whole Object Is Harder
Java already gives you tools for moving primitive values in and out of a program. PrintWriter, Scanner, DataInputStream, and DataOutputStream can each handle Java's primitive types with little effort. A number such as 92 has a direct binary form that a stream can carry. An object is different. It bundles several values together, and some of those values may themselves be objects. This article answers a simple question: what can a program do when the thing it wants to store or send is a whole object rather than a single primitive value?
Before reading the rest, try to predict the difficulty. Suppose you have a Student object with a name and a score, and you want to save it to a file so that another run of the program can load it back. Think about what you would have to write out, and what you would have to do when reading the data back in to rebuild an equivalent Student.
Hints
- Consider each field of the Student separately. How would you turn the name into something a stream can carry?
- Consider the order in which you would write the values, and whether reading them back needs to follow that same order.
Following a Student Through Serialization
The diagram below traces the Student object from the prediction above. On the left is the object as it exists in the running program. In the middle is the form it takes once it has been serialized into a stream. On the right is the equivalent copy that exists after the object has been read back. The diagram shows only the endpoints of each change. The mechanism that performs the change belongs to the object stream classes, which are introduced in the sections that follow.
- 1Start with a Student object that holds a name and a score in the running program.
- 2
writeObjectsends the Student into the stream. The fields are encoded as a sequence of primitive-type data values, so they can be output as bytes. - 3The stream now holds only the encoded bytes. The original object is still in the running program, but the bytes are what travel to a file or another program.
- 4
readObjectreads the bytes back. The result is a reconstituted copy of the original Student, not the same object in memory.
What Serialization Means and Why It Helps
Serializing an object means encoding it as a sequence of primitive-type data values so it can be output as bytes or characters. Reading it back reconstitutes a copy of the original object.
Done by hand, this is a major chore. You would decide how each field becomes a value, write those values in a fixed order, and on input read them back in exactly that order and assemble the object again. For complex objects with many fields, this bookkeeping grows quickly. The classes ObjectInputStream and ObjectOutputStream exist so that Java can do much of this encoding and decoding for you. That is why writing an object to a stream requires serialization: a stream carries values, and serialization turns an object into the values a stream can carry.
Wrapping Object Streams Around Any Byte Stream
ObjectInputStream and ObjectOutputStream are subclasses of InputStream and OutputStream, and they are wrapper classes. A wrapper does not know or care where its bytes go. You can wrap it around an arbitrary InputStream or OutputStream, which makes object input and output possible on any byte stream. The object stream adds object-aware behavior on top of a stream you already have.
Two methods carry the object traffic. writeObject(Object obj), defined in ObjectOutputStream, writes an object to the stream. readObject(), defined in ObjectInputStream, reads an object back. Both methods can throw IOException, so the calling code must handle that possibility. The example below wraps an ObjectOutputStream around a FileOutputStream, which is one arbitrary byte stream. Wrapping a different byte stream would change only the inner part of that expression.
Following a Mixed Stream Step by Step
An object stream is not limited to objects. ObjectOutputStream also has methods such as writeInt() and writeDouble() for primitive values, and ObjectInputStream has corresponding methods for reading them back. Primitive values can be interspersed with objects in the same data, so one stream can hold a count, then an object, then a measurement. The diagram shows the order of the messages for that three-part stream.
- 1After
writeInt(count), the stream holds one item: the binary form of the int. - 2After
writeObject(student), the stream holds two items: the int, then the encoded Student. - 3After
writeDouble(average), the stream holds three items: the int, the Student, then the double. - 4The reader must take the items out in the same order:
readInt, thenreadObject, thenreadDouble.
Inside the file, primitive values are stored in their internal binary format. A writeInt() call therefore records the binary representation of an int, not the characters of its decimal digits. The stream does not label what it contains, so the reading code is responsible for asking for items in the right order.
What do you think happens?
If the reading code calls readDouble() first on the file written above, what do you think happens?
Reveal answer
The reader does not get the intended double. It asks for a double when the next item in the stream is the int, so it does not recover the data that was meant to be read there.
The stream does not label what it contains. The reader has to request items in the same order they were written, because the stream only knows the position it is at, not what each item was meant to be.
Why the Cast After readObject() Is Needed
readObject() returns type Object, which is the most general type in Java. The stream can hand back any object, so the method cannot promise that the result is a Student. To use the result as a Student, you must cast it. In the statement below, the cast (Student) sits between the assignment and the call to readObject(). It tells the compiler that you expect the object to be a Student.
Student s = (Student) in.readObject();- 1
in.readObject()reads the next object from the stream and returns it as type Object. - 2The cast (Student) tells the compiler to treat that Object as a Student.
- 3The result is assigned to s, which can then use Student methods and fields.
- Student s: declares a variable of the specific type you want to work with
- (Student): the cast, which narrows the Object returned by
readObject()to a Student in.readObject(): reads the next object from theObjectInputStream, typed as Object
Mistakes Beginners Make with Object Streams
Common mistake Reading items in a different order than they were written
What it looks likeWriting an int, then an object, then a double, but reading the double first.
Why it failsThe stream does not label its contents, so the reader receives whatever item is next, not the one it intended.
Do this insteadRead the items in exactly the order they were written, and keep the two sequences side by side while you code.
Common mistake Forgetting the cast after readObject()
What it looks likeWriting Student s = in.readObject(); without a cast.Why it failsreadObject()returns type Object, so the result is not known to be a Student.Do this insteadAdd the (Student) cast so the returned object can be used as a Student.
Common mistake Ignoring IOException from the object I/O calls
What it looks likeCalling writeObject() or readObject() with no handling for failures.
Why it failsBoth methods can throw IOException, so the calling code must be ready for that possibility.
Do this insteadHandle IOException in the calling code, as the examples in this article do by declaring it or using a structured handler.
Common mistake Expecting to read object data as plain text
What it looks likeOpening the student.dat file in a text editor and expecting to see the name and score.
Why it failsObject streams are byte streams whose objects are represented in binary, machine-readable form.
Do this insteadRead the data back through an ObjectInputStream. If you need human-readable output, consider a character-based approach.
Limitations of Object Streams
Object streams are byte streams, and their objects are represented in binary, machine-readable form. This is good for efficiency. The same binary nature brings the fragility often seen in binary data: small changes in the bytes can make the data hard to interpret. This is part of why binary formats are less forgiving than text.
A second limitation is that the binary format of Java objects is very specific to Java. Data in object streams is not easily available to programs written in other programming languages. A reader written in another language cannot simply interpret the bytes without knowing this Java-specific format. For long-term storage and for communication with non-Java programs, other approaches to serialization are usually better. The source points to a character-based approach as one alternative.
| Consideration | Object streams | Character-based or other text approach |
|---|---|---|
| Form of the data | Binary, machine-readable bytes | Characters |
| Fragility | Small byte changes can make data hard to interpret | Less forgiving problems than binary; described as more forgiving |
| Use by other languages | Not easily available to programs in other languages | Usually the better choice for non-Java programs |
| Typical fit | Efficient exchange between Java programs | Long-term storage and communication with non-Java programs |
Object streams compared with a character-based approach, based on the limitations described in the source.
Sending an Object Between Two Java Programs
The same stream classes can carry an object across a network connection. The sending program wraps an ObjectOutputStream around the byte stream of the connection and calls writeObject. The receiving program wraps an ObjectInputStream around its end of the connection and calls readObject. Because object streams wrap any byte stream, the wrappers at each end do not need to know that the bytes travel over a network. The diagram shows the path the object takes.
Choosing Between Object Streams and Other Approaches
Use object streams when both ends of the exchange are Java programs and you want the encoding and decoding handled for you, especially for a whole object with several fields. Consider other approaches when the data must be read by programs written in other languages, when it must be kept for long-term storage, or when you need a human-readable record. The source states that other approaches to serialization are usually better in those cases.
Practice: Saving a Score Record
A program needs to save one record for a run: an int attempt number, a Student object with a name and a score, and a double average. Write the write sequence and the matching read sequence using an ObjectOutputStream and an ObjectInputStream around a file byte stream. Then explain what would go wrong if the reader asked for the double before the object.
Hints
- Write the three items in the order listed, and make the reader ask for them in that same order.
- Remember that
readObject()returns type Object, so the object read needs a cast to Student. - Think about what the reader would receive if the next item in the stream were the object rather than the double.
Key Takeaways
- Serializing an object encodes it as a sequence of primitive-type data values so it can be output as bytes or characters, and reading it back reconstitutes a copy of the original object.
ObjectOutputStreamandObjectInputStreamare wrapper classes that can wrap anyOutputStreamorInputStream. They usewriteObject(Object obj)andreadObject(), both of which can throw IOException.- Primitive values such as
writeInt()andwriteDouble()can be mixed with objects in one stream, and reads must follow the order of writes because the stream does not label its contents. readObject()returns type Object, so the result usually needs a cast to the specific class.- Object streams are efficient binary streams specific to Java and less forgiving than text, so other approaches are usually better for long-term storage and for communication with non-Java programs.