Concepts / ObjectInputStream and ObjectOutputStream

ObjectInputStream and ObjectOutputStream

Serializable is a marker interface: it declares no methods and exists only to indicate that an object may be written and read.

  • Programming

Why a Live Object Cannot Simply Be Saved

Suppose you have a Java object that holds a student's name, grades, and enrollment date, and you want to keep it after the program ends. A program cannot simply write a live object to a file by pointing at it. The object lives in the running program's memory, and a file can only hold a sequence of bytes. Java provides object streams for this job, and the Serializable interface is the piece that tells Java which objects are allowed to pass through those streams.

Consider a simpler case first. A primitive value such as the number 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. So the real question this article answers is what a program must do, and what a class must declare, so that a whole object rather than a single number can be stored and later rebuilt.

EASY

Predict the difficulty before reading on. You have a Student object with a name and a score, and you want to save it to a file so another run of the program can load it back. Think about what you would have to write out, in what order, and what you would have to do when reading the data back in to rebuild an equivalent Student.

Hints
  • Think about each field: how would you turn it into a value a file can hold?
  • Think about what happens when reading: how does the reader know which value comes next?

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 objects with many fields, that bookkeeping grows quickly. The classes ObjectInputStream and ObjectOutputStream exist so that Java can do much of this encoding and decoding for you.

Tracing an Object's Path Through the Stream

It helps to trace an object's path step by step. The object begins as an ordinary value in memory. Its class decides whether the object is eligible for the object streams. When the class carries the Serializable mark, the object stream can accept the object, write it out, and later read it back. When the mark is missing, the stream has no permission to handle that object. The diagram below follows this path so you can see where the Serializable declaration sits in the process.

Student objectlive in the running programNot writtenstream has no permissionSerializable mark?checked from the classObject streamwriteObject(obj)Byte sequencefield values in binaryRebuilt Student copyreadObject() then castasks eligibilityno markmark presentencodesdecodes
How does an in-memory Student become a sequence of bytes on the stream, and how is it rebuilt into an equivalent object on the other side?

Read the diagram from left to right and notice the decision point. Serialization is the encoding step: the object's contents become a sequence of primitive-type data values that can be output as bytes. Deserialization is the reverse: reading those bytes reconstitutes a copy of the original object. The copy has the same contents as the original, but it is a new object, not the old one still sitting in memory.

Serializable as a Marker Interface

Most interfaces you may have seen promise a set of methods that an implementing class must provide. Serializable works differently. It declares no methods at all. It exists only as a marker: its purpose is to indicate a property of the class, namely that its objects are meant to be written and read. Because there are no methods to write, making a class serializable requires very little work.

A marker interface is best understood by contrast. An interface that declares behavior is a contract: a class that implements it must supply specific methods, and the code that uses the class can call those methods. A marker interface makes no such demand. It attaches a label to the class, and the object stream checks for that label before it accepts an object. The label carries information, but it carries no behavior of its own.

Key point

Serializable is a marker interface: it declares no methods and exists only to indicate that an object may be written and read. Its presence is the permission; there is no code to implement.

Adding the mark to a class is a single change to its header. Java's standard library already uses this approach, and many of its classes are declared serializable in exactly this way. The clause sits after the class name, in the same position where other implements clauses appear.

java
Notes on the code
  • The implements Serializable clause is added after the class name, like any other implements clause.
  • No methods are added to satisfy the interface, because Serializable declares none.

When Instance Variables Block Serialization

Adding the clause to the outer class is not the whole job. Every instance variable in a serializable object must also be serializable. A class can declare the mark correctly and still produce an object that cannot be saved, because one of its fields has a type that is not serializable. The mark on the outer class is not enough by itself.

Worked example

One Field That Blocks a Whole Object

A Course class implements Serializable. It has a name field and a teacher field, and the teacher field holds a Teacher object. The Teacher class does not implement Serializable. Can a Course be written to an object stream?

  1. 1
    Check the outer class

    Course declares implements Serializable, so the class-level mark is present. Considered alone, Course looks eligible.

  2. 2
    Check each instance variable

    The name field is part of the Course's contents and must also be serializable. The teacher field holds a Teacher, and Teacher has no Serializable mark.

  3. 3
    Apply the rule

    Every instance variable must be serializable. The teacher field breaks that rule, so the object's contents cannot all be encoded, even though the Course class itself is marked correctly.

  4. 4
    Fix the problem

    Either make Teacher serializable by adding implements Serializable to its class, or restructure Course so it does not hold a non-serializable Teacher in a field that must be saved.

Result

The Course cannot be serialized as it stands. The fix must address the non-serializable field, not the outer class, which was already correct.

Wrapping a Byte Stream with Object Streams

ObjectInputStream and ObjectOutputStream are subclasses of InputStream and OutputStream, and they are wrapper classes. A wrapper does not care where its bytes go. It is wrapped 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.

Programcalls writeObjectObjectOutputStreamobject-aware encodingByte streamfor exampleFileOutputStreamStoragefile or other destinationobjects and primitivesbytesraw bytes
How do ObjectOutputStream and ObjectInputStream sit on top of a lower-level byte stream such as a FileOutputStream, and which layer handles object encoding versus raw bytes?

The diagram shows the division of labor. The object stream layer handles the encoding of objects into primitive values and binary form. The lower layer handles only raw bytes and does not need to know anything about Java objects. This is why the same object stream code can be used whether the bytes end up in a file or somewhere else.

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 of these methods can throw IOExceptions, so the calling code must handle that possibility.

java
Notes on the code
  • The object stream wraps a lower-level FileOutputStream, which carries the raw bytes.
  • readObject() returns type Object, so the result is cast to Student.
  • Both calls can throw IOException, so real code must handle it.

Mixing Primitives and Objects in One Stream

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 a single stream can hold a count, then an object, then a measurement.

Writing codeObject streamReading code1. writeInt(count)2. writeObject(student)3. writeDouble(average)4. readInt()5. (Student) readObject()6. readDouble()
When primitives and objects share a stream in a given order, how must the reads follow that exact order, and where does the cast after readObject() fit?

Read the sequence from top to bottom. The writing side produces three items in a fixed order, and the reading side must consume them in that same order. The stream does not label what it contains. A reader that asks for an int when the next item is an object will not recover the intended data. The cast in step 5 is needed because readObject() returns type Object, and the reader must tell Java which class to treat the result as.

java
Notes on the code
  • The write order is int, object, double, and the reads follow exactly that order.
  • The cast (Student) is applied to the result of readObject() because it returns Object.
  • Swapping the two reads would fail to recover the intended values.

Inside the file, primitive values are stored in their internal binary format. A writeInt() call records the binary representation of an int, not the characters of its decimal digits. Keeping that in mind explains why the byte content is compact but not something a person can read directly.

Mistakes Beginners Make with Object Streams

  • Common mistake

    Adding implements Serializable to the outer class and assuming the whole object is saved

    What it looks like

    Course implements Serializable, but its teacher field holds a Teacher object whose class is not serializable.

    Why it fails

    Every instance variable in a serializable object must also be serializable, so the outer mark is not enough by itself.

    Do this instead

    Check each field's type and make every type in the object's contents serializable, or leave that field out of what is written.

  • Common mistake

    Reading values 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 comes next, not the item it asked for.

    Do this instead

    Read the items in exactly the order they were written.

  • 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 compiler needs the cast to treat the result as a Student.

    Do this instead
    Write Student s = (Student) in.readObject();
  • Common mistake

    Assuming the mark makes a class saveable without any other work

    What it looks like

    Declaring a class serializable and then expecting its objects to work in any situation.

    Why it fails

    The mark only indicates permission; the rule about instance variables still applies, and the trade-offs of the binary format still exist.

    Do this instead

    Confirm the contents of the object and decide whether a binary object stream is the right tool at all.

Limitations and Choosing an Approach

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, which is part of why binary formats are less forgiving than text.

A second limitation is that the binary format of Java objects is specific to Java. Data in object streams is not easily available to programs written in other languages, because a reader in another language cannot interpret the bytes without knowing this Java-specific format. For long-term storage and for communication with non-Java programs, other approaches are usually better. A character-based approach is one alternative, and object streams are used between Java programs in network examples.

ConcernObject streamsCharacter-based or text approach
EfficiencyGood, because objects are stored in compact binary formGenerally less compact, since data is written as characters
ReadabilityHard to read directly; small byte changes can make data hard to interpretEasier for people to inspect and check
Use by other languagesNot easily available, because the format is specific to JavaUsually a better fit for communication with non-Java programs
Typical rolePassing objects between Java programsLong-term storage and cross-language exchange

Trade-offs to weigh when choosing between object streams and other approaches

MEDIUM

A team is writing two programs. One is a Java game server that sends player objects to a Java client. The other is a reporting tool written in Python that must read the same records later. Which approach would you choose for the Python reporting tool, and why would object streams be a poor fit for it?

Hints
  • Ask which programs must be able to read the data without knowing Java's specific binary format.
  • Object streams are good for efficiency between Java programs; think about what the Python reader needs.

Key Takeaways

  • Serializing an object encodes it as primitive-type data values that can be output as bytes; reading it back reconstitutes a copy of the original object.
  • Serializable is a marker interface that declares no methods. A class becomes serializable by adding implements Serializable, but every instance variable must also be serializable.
  • ObjectOutputStream and ObjectInputStream are wrapper classes around any byte stream, using writeObject and readObject, and they can interleave primitive writes such as writeInt and writeDouble.
  • Reads must follow the exact order of writes, and readObject() returns Object, so it needs a cast; both methods can throw IOException.
  • Object streams are efficient and convenient between Java programs, but their binary, Java-specific format limits readability and use by other languages.