Concepts / Network Communication with Object Streams

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.

  • Programming

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?

MEDIUM

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.

Student objectname and score in therunning programSerialized formvalues encoded as bytesRebuilt Studentequivalent copy afterreadingwriteObjectreadObject
What happens to the Student's name and score when writeObject sends it into a stream, and how readObject rebuilds an equivalent copy?
  1. 1
    Start with a Student object that holds a name and a score in the running program.
  2. 2
    writeObject sends the Student into the stream. The fields are encoded as a sequence of primitive-type data values, so they can be output as bytes.
  3. 3
    The 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. 4
    readObject reads 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

Definition

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.

java
java

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.

Writing programwrites three items in orderReading programreads three items in thesame order1. writeInt(count)2. writeObject(student)3. writeDouble(average)
In what order are primitives and objects written, and what must the reader do to match that order?
java
java
  1. 1
    After writeInt(count), the stream holds one item: the binary form of the int.
  2. 2
    After writeObject(student), the stream holds two items: the int, then the encoded Student.
  3. 3
    After writeDouble(average), the stream holds three items: the int, the Student, then the double.
  4. 4
    The reader must take the items out in the same order: readInt, then readObject, then readDouble.

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

Code walkthrough
Student s = (Student) in.readObject();
  1. 1
    in.readObject() reads the next object from the stream and returns it as type Object.
  2. 2
    The cast (Student) tells the compiler to treat that Object as a Student.
  3. 3
    The 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 the ObjectInputStream, typed as Object

Mistakes Beginners Make with Object Streams

  • Common mistake

    Reading items in a different order than they were written

    What it looks like

    Writing an int, then an object, then a double, but reading the double first.

    Why it fails

    The stream does not label its contents, so the reader receives whatever item is next, not the one it intended.

    Do this instead

    Read 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 like
    Writing Student s = in.readObject(); without a cast.
    Why it fails

    readObject() returns type Object, so the result is not known to be a Student.

    Do this instead

    Add 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 like

    Calling writeObject() or readObject() with no handling for failures.

    Why it fails

    Both methods can throw IOException, so the calling code must be ready for that possibility.

    Do this instead

    Handle 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 like

    Opening the student.dat file in a text editor and expecting to see the name and score.

    Why it fails

    Object streams are byte streams whose objects are represented in binary, machine-readable form.

    Do this instead

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

ConsiderationObject streamsCharacter-based or other text approach
Form of the dataBinary, machine-readable bytesCharacters
FragilitySmall byte changes can make data hard to interpretLess forgiving problems than binary; described as more forgiving
Use by other languagesNot easily available to programs in other languagesUsually the better choice for non-Java programs
Typical fitEfficient exchange between Java programsLong-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.

Sending programholds the object to sendObjectOutputStreamwraps the outgoing bytestreamNetwork byte streamany OutputStream andInputStream pairObjectInputStreamwraps the incoming bytestreamReceiving programrebuilds an equivalentobjectwriteObjectencoded bytesencoded bytesreadObject
How does an object move from the sending program through the connection to the receiving program, and which stream wrappers sit at each end?

Choosing Between Object Streams and Other Approaches

Best practice

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

MEDIUM

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.
  • ObjectOutputStream and ObjectInputStream are wrapper classes that can wrap any OutputStream or InputStream. They use writeObject(Object obj) and readObject(), both of which can throw IOException.
  • Primitive values such as writeInt() and writeDouble() 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.