Serialization ·
‹ Previous Next ›
⏱ 5 min read Modified: 2026-11-01

Serialization and Deserialization in Java

You mark a field as transient, write the object to a file, read it back — and bird.getRing().getSize() fails with a NullPointerException. The reason: a transient field is not restored during deserialization, and the object constructor is not called, so the reference stays null. Let us see how this mechanism works.

Serialization in Java is the process of converting the state of an object into a sequence of bytes that can be saved to a file or a database or sent over the network. Deserialization is the reverse process: rebuilding the object from those bytes. An object can be serialized with the standard Java tools if its class implements the java.io.Serializable interface.

1. What is serialization and deserialization in Java

Serialization turns an object from program memory into a format that can be stored or transferred — a stream of bytes. Deserialization rebuilds an object with the same state from that stream. To serialize an object means to write it to a byte stream; to deserialize it means to read the bytes and get the object back.

Why you need it in practice:

  • to keep the state of a program between runs (a shopping cart, user settings);
  • to send an object to another process or machine (Java RMI, distributed caches, HTTP sessions in a cluster);
  • to make a deep copy of an object (write it and read it back right away).

Built-in serialization in Java is provided by the Java Serialization API in the java.io package: the ObjectOutput and ObjectInput interfaces and the classes that implement them, ObjectOutputStream and ObjectInputStream. Both are wrappers over ordinary byte streams.

2. How to serialize an object in Java

To serialize an object in Java, its class must implement the Serializable interface, and the object itself is passed to the writeObject() method of ObjectOutputStream. To deserialize it, call ObjectInputStream.readObject() and cast the result to the required type.

The java.io.Serializable interface has no methods at all — it is a marker interface. It only tells the JVM that objects of the class may be serialized. If you try to serialize an object whose class does not implement Serializable, you get a java.io.NotSerializableException.

import java.io.Serializable;

public class Cat implements Serializable {
    private String name;

    public Cat(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }
}

Steps to serialize an object:

  1. Create an OutputStream (for example, a FileOutputStream).
  2. Pass it to the ObjectOutputStream constructor.
  3. Call writeObject(obj) — the object is serialized and written to the stream.

The streams are closed with try-with-resources:

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;

public class SerializeCat {
    private static final String FILE_NAME = "testSer.ser";

    public static void main(String[] args) {
        serialize();
        Cat cat = deserialize();
        System.out.println("Restored cat: " + cat.getName());
    }

    private static void serialize() {
        Cat cat = new Cat("Tom");
        try (FileOutputStream fs = new FileOutputStream(FILE_NAME);
             ObjectOutputStream os = new ObjectOutputStream(fs)) {
            os.writeObject(cat);
        } catch (IOException e) {
            System.out.println(e.getMessage());
        }
    }

    private static Cat deserialize() {
        Cat cat = null;
        try (FileInputStream fis = new FileInputStream(FILE_NAME);
             ObjectInputStream ois = new ObjectInputStream(fis)) {
            cat = (Cat) ois.readObject();
        } catch (IOException | ClassNotFoundException e) {
            System.out.println(e.getMessage());
        }
        return cat;
    }
}
// Output: Restored cat: Tom

The serialized object can be restored in another program and on another machine, as long as the same class with a compatible serialVersionUID is on its classpath. Otherwise readObject() throws ClassNotFoundException or InvalidClassException.

3. How deserialization works

During deserialization the JVM allocates memory for the object and fills its fields with values from the stream without calling the constructor of the serializable class. That is why initialization logic from the constructor (checks, default values, opening resources) does not run when an object is read.

What happens to different kinds of fields:

Field Written to the stream? Value after deserialization
Regular field (primitive or Serializable object) Yes The value from the stream
transient No The default value: null, 0, false
static No The current value of the static field in this JVM (it belongs to the class, not to the object)
Field of a non-serializable superclass No Whatever the no-arg constructor of that superclass assigns (see inheritance)
Reference to an object whose class is not Serializable, without transient — Serialization fails: NotSerializableException in writeObject()

Access modifiers do not matter: private fields are saved just like public ones. A field with an initializer (private transient int count = 10;) is still 0 after deserialization — field initializers run as part of the constructor, and the constructor is not called.

4. The object graph

The object graph is the object itself plus every object reachable from it through references, including references inside nested objects. Serialization writes the whole graph, so every object in it must be serializable, and deserialization restores the whole graph together with the links between objects.

If several references point to the same object, they also point to one object in the restored graph. The serialization mechanism remembers objects it has already written and, when it meets one again, writes only a reference to it. This is how shared and circular references are saved correctly and no object is written twice.

import java.io.Serializable;

public class Dog implements Serializable {
    private Collar theCollar;

    public Dog(Collar collar) {
        theCollar = collar;
    }

    public Collar getCollar() {
        return theCollar;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Dog dog = (Dog) o;
        return theCollar != null ? theCollar.equals(dog.theCollar) : dog.theCollar == null;
    }

    @Override
    public int hashCode() {
        return theCollar != null ? theCollar.hashCode() : 0;
    }
}
import java.io.Serializable;

public class Collar implements Serializable {
    private int collarSize;

    public Collar(int size) {
        collarSize = size;
    }

    public int getCollarSize() {
        return collarSize;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Collar collar = (Collar) o;
        return getCollarSize() == collar.getCollarSize();
    }

    @Override
    public int hashCode() {
        return getCollarSize();
    }
}

What will this code print? Pay attention to the last two lines of main().

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;

public class SerializeDog {
    private static final String FILE_NAME = "testSer.ser";

    public static void main(String[] args) {
        Collar collar = new Collar(3);
        Dog dog1 = new Dog(collar);
        Dog dog2 = new Dog(collar);
        serialize(dog1, dog2);
        Dog[] deserializedDogs = deserialize(2);

        System.out.println(dog1.equals(deserializedDogs[0]));
        System.out.println(dog2.equals(deserializedDogs[1]));
        Collar desCollar1 = deserializedDogs[0].getCollar();
        Collar desCollar2 = deserializedDogs[1].getCollar();
        System.out.println(desCollar1.equals(desCollar2));
        System.out.println(desCollar1 == desCollar2);
        System.out.println(dog1 == deserializedDogs[0]);
    }

    private static void serialize(Dog... dogs) {
        try (FileOutputStream fs = new FileOutputStream(FILE_NAME);
             ObjectOutputStream os = new ObjectOutputStream(fs)) {
            for (Dog dog : dogs) {
                os.writeObject(dog);
            }
        } catch (IOException e) {
            System.out.println(e.getMessage());
        }
    }

    private static Dog[] deserialize(int dogNumber) {
        Dog[] dogs = new Dog[dogNumber];
        try (FileInputStream fs = new FileInputStream(FILE_NAME);
             ObjectInputStream os = new ObjectInputStream(fs)) {
            for (int i = 0; i < dogNumber; i++) {
                dogs[i] = (Dog) os.readObject();
            }
        } catch (IOException | ClassNotFoundException e) {
            System.out.println(e.getMessage());
        }
        return dogs;
    }
}
// Output:
// true
// true
// true
// true   - after deserialization both dogs refer to ONE Collar object
// false  - deserialization always creates a new object

Easy to miss

A shared reference survives only within one ObjectOutputStream. If you write dog1 and dog2 to two different streams (or files), each dog gets its own separate Collar object after reading, and desCollar1 == desCollar2 becomes false.

5. The transient keyword in Java

The transient keyword excludes a field from serialization: its value is not written to the stream, and after deserialization the field gets the default value of its type. Use it for fields that should not or cannot be saved: passwords, caches, computed values, references to non-serializable objects.

If a field refers to an object whose class does not implement Serializable, you have two options: make that class serializable or mark the field as transient. If you do neither, writeObject() throws java.io.NotSerializableException with the name of the non-serializable class.

import java.io.Serializable;

public class Bird implements Serializable {
    private String name;
    private transient Ring ring;

    public Bird(String name, Ring ring) {
        this.name = name;
        this.ring = ring;
    }

    public Ring getRing() {
        return ring;
    }
}
public class Ring {   // does not implement Serializable
    private int size;

    public Ring(int size) {
        this.size = size;
    }

    public int getSize() {
        return size;
    }
}
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;

public class SerializeBird {
    private static final String FILE_NAME = "testSer.ser";

    public static void main(String[] args) {
        Bird bird = new Bird("pigeon", new Ring(3));
        System.out.println("Ring size before serialization: "
                + bird.getRing().getSize());
        try (FileOutputStream fs = new FileOutputStream(FILE_NAME);
             ObjectOutputStream os = new ObjectOutputStream(fs)) {
            os.writeObject(bird);
        } catch (IOException e) {
            System.out.println(e.getMessage());
        }
        try (FileInputStream fis = new FileInputStream(FILE_NAME);
             ObjectInputStream ois = new ObjectInputStream(fis)) {
            bird = (Bird) ois.readObject();
        } catch (IOException | ClassNotFoundException e) {
            System.out.println(e.getMessage());
        }
        // bird.getRing().getSize() would throw NullPointerException here
        System.out.println("Ring after deserialization: " + bird.getRing());
    }
}
// Output:
// Ring size before serialization: 3
// Ring after deserialization: null

If you remove transient from the ring field, the program never gets to reading: writeObject() throws NotSerializableException, and the console shows the message Ring (the class name). How to save the value of a transient field by hand is shown in the section writeObject() and readObject().

6. serialVersionUID and class versions

serialVersionUID is the version number of a serializable class, written to the stream together with the object. During deserialization the JVM compares the number from the stream with the number of the class on the classpath; if they differ, readObject() throws java.io.InvalidClassException.

If the field is not declared explicitly, the JVM computes it from the structure of the class (name, interfaces, fields, methods). Any change to the class — even adding a method — can change the computed value, and different compilers may compute it differently. So it is better to declare the version explicitly:

import java.io.Serial;
import java.io.Serializable;

public class Cat implements Serializable {
    @Serial // Java 14+: the compiler checks the declaration
    private static final long serialVersionUID = 1L;

    private String name;
    // ...
}

If you write an object with serialVersionUID = 1L, change the value in the class to 2L and try to read the file, you get:

java.io.InvalidClassException: Cat; local class incompatible:
stream classdesc serialVersionUID = 1, local class serialVersionUID = 2

With an explicit serialVersionUID that stays the same, compatible changes (adding a field, removing a field) do not break reading old data: a new field gets its default value, and an extra field from the stream is ignored. Change the number only when old data really cannot be read by the new version of the class (for example, the type of a field has changed).

7. Deserialization and inheritance

If a serializable class extends a class that does not implement Serializable, the fields of that superclass are not saved. During deserialization they are initialized by the no-arg constructor of the nearest non-serializable superclass, while the fields of the serializable class itself are restored from the stream. The constructor of the deserialized class is still not called.

The no-arg constructor of the non-serializable superclass must exist and be accessible from the subclass. If it is missing, readObject() throws java.io.InvalidClassException with a message like Bug; no valid constructor.

import java.util.Objects;

public class Insect {   // does not implement Serializable
    private String name;

    public Insect(String type) {
        this.name = type;
    }

    public Insect() {
    }

    public String getName() {
        return name;
    }

    public void setName(String type) {
        this.name = type;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Insect insect = (Insect) o;
        return Objects.equals(name, insect.name);
    }

    @Override
    public int hashCode() {
        return Objects.hash(name);
    }

    @Override
    public String toString() {
        return "Insect{name=" + name + "}";
    }
}
import java.io.Serializable;
import java.util.Objects;

public class Bug extends Insect implements Serializable {
    private boolean fly;

    public Bug(String type, boolean fly) {
        super(type);
        this.fly = fly;
    }

    public boolean isFly() {
        return fly;
    }

    public void setFly(boolean fly) {
        this.fly = fly;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        if (!super.equals(o)) return false;
        Bug bug = (Bug) o;
        return fly == bug.fly;
    }

    @Override
    public int hashCode() {
        return Objects.hash(super.hashCode(), fly);
    }

    @Override
    public String toString() {
        return "Bug{fly=" + fly + "} " + super.toString();
    }
}
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;

public class SerializeInsectExample {
    private static final String FILE_NAME = "testSer.ser";

    public static void main(String[] args) {
        Bug bug = new Bug("May beetle", true);
        System.out.println("Before serialization: " + bug);
        try (FileOutputStream fs = new FileOutputStream(FILE_NAME);
             ObjectOutputStream os = new ObjectOutputStream(fs)) {
            os.writeObject(bug);
        } catch (IOException e) {
            System.out.println(e.getMessage());
        }
        try (FileInputStream fis = new FileInputStream(FILE_NAME);
             ObjectInputStream ois = new ObjectInputStream(fis)) {
            bug = (Bug) ois.readObject();
        } catch (IOException | ClassNotFoundException e) {
            System.out.println(e.getMessage());
        }
        System.out.println("After deserialization: " + bug);
    }
}
// Output:
// Before serialization: Bug{fly=true} Insect{name=May beetle}
// After deserialization: Bug{fly=true} Insect{name=null}

The fly field belongs to the serializable class Bug and is restored from the stream. The name field is declared in the non-serializable Insect, so it gets its value from the Insect() constructor, that is null. If you remove the no-arg constructor from Insect, writing still succeeds, but reading ends with InvalidClassException.

8. Serializable vs Externalizable

The Externalizable interface extends Serializable and gives the programmer full control over the format: the class writes and reads its own data in the writeExternal() and readExternal() methods. With standard serialization (Serializable) the fields are written automatically.

Feature Serializable Externalizable
Methods to implement None (marker); optionally private writeObject() / readObject() Required: writeExternal(ObjectOutput) and readExternal(ObjectInput)
What is written All non-static, non-transient fields, automatically Only what writeExternal() writes explicitly
Constructor on reading Not called (except the constructor of a non-serializable superclass) The public no-arg constructor is called; without it — InvalidClassException
Role of transient Excludes the field Irrelevant: the code of writeExternal() decides
When to choose Almost always: simple and reliable You need a compact or special format, or speed matters

Partial control without switching to Externalizable comes from private writeObject() and readObject() methods in the serializable class itself. The JVM calls them instead of the default logic. For example, this is how the ring size from the transient field of Bird can be saved:

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serial;
import java.io.Serializable;

public class Bird implements Serializable {
    private String name;
    private transient Ring ring;

    // constructor and getRing() - as in section 5

    @Serial
    private void writeObject(ObjectOutputStream out) throws IOException {
        out.defaultWriteObject();                     // regular fields (name)
        out.writeInt(ring != null ? ring.getSize() : -1);
    }

    @Serial
    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
        in.defaultReadObject();
        int size = in.readInt();
        ring = size >= 0 ? new Ring(size) : null;
    }
}
// SerializeBird now prints: Ring after deserialization: Ring@...
// and bird.getRing().getSize() returns 3

Records are different

Records (record, Java 16+) are also serialized through Serializable, but they are deserialized differently: the JVM calls the canonical constructor of the record. This means the checks in the constructor also run when reading from a stream, and writeObject()/readObject() methods are ignored for records.

9. Serialization vs marshalling

Marshalling is preparing data for transfer between processes or systems, while serialization is converting an object into a stream of bytes. In everyday speech the two terms are often used as synonyms, but marshalling is broader: it can include extra information needed for the transfer (in Java RMI, for example, the object travels together with the location its class can be loaded from).

In Java the word marshalling usually shows up in two contexts:

  • Java RMI — the arguments and results of a remote call are marshalled with standard serialization;
  • JAXB — the Marshaller class turns an object into XML, and Unmarshaller turns it back.

Built-in Java serialization produces a binary format that only Java programs understand. To exchange data with other systems, objects are usually serialized to JSON (the Jackson and Gson libraries), XML or Protocol Buffers: these formats do not depend on the language and do not execute code when read.

10. Why an object is not serializable: common mistakes

Most often an object is not serializable because its graph contains an object whose class does not implement Serializable: writeObject() throws NotSerializableException with the name of that class. The other typical errors appear on reading — because of a changed class version, a missing constructor, or a mismatch between the number of objects written and read.

Symptom Cause How to fix
NotSerializableException: Ring on writing A field refers to an object whose class is not Serializable (at any level of the graph) Implement Serializable in that class or mark the field transient
NotSerializableException with the name of the outer class An instance of a non-static inner or anonymous class is serialized: it holds a hidden reference to the outer object Make the nested class static or move it to a separate file
NotSerializableException for a lambda in a field A regular lambda is not serializable Do not keep lambdas in serializable fields, or mark the field transient
InvalidClassException: ... local class incompatible The serialVersionUID has changed (explicit or computed) Declare serialVersionUID explicitly and do not change it without a reason
InvalidClassException: ... no valid constructor The non-serializable superclass has no accessible no-arg constructor Add such a constructor to the superclass
ClassNotFoundException on reading The class is not on the classpath of the reading program Add the same class (same package and name)
EOFException More objects are read than were written Write a collection as one object, or write the number of elements first
NullPointerException after reading The field was transient and became null Check for null or restore the field in readObject()

All these exceptions except NullPointerException are checked subclasses of IOException or ReflectiveOperationException, so the compiler makes you handle them — see Java exceptions and the overview of common Java exceptions.

11. Deserialization security

Deserializing data from an untrusted source is dangerous: readObject() creates objects of any serializable class on the classpath, and a specially crafted stream can trigger a chain of calls leading to arbitrary code execution or memory exhaustion. That is why built-in Java serialization must not be applied to data that comes from a user or over the network without checks.

What to do:

  • do not accept serialized Java objects from outside; use JSON or Protobuf for data exchange;
  • if deserialization cannot be avoided, restrict the allowed classes with an ObjectInputFilter (JEP 290, Java 9; backported to 8u121). Java 17 added filter factories for the whole application (JEP 415);
  • validate the object state in readObject() the same way you do in the constructor.
ObjectInputFilter filter = ObjectInputFilter.Config.createFilter(
        "com.example.model.*;java.base/*;maxdepth=10;!*");

try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(FILE_NAME))) {
    ois.setObjectInputFilter(filter);
    Object obj = ois.readObject(); // class not in the list -> InvalidClassException: filter status: REJECTED
}

The same filter can be set for the whole JVM without changing the code — with the system property -Djdk.serialFilter=....

Frequently asked questions

Are private fields and fields without getters serialized?

Yes. Built-in serialization works with fields directly and bypasses getters and setters, so access modifiers make no difference. Only static and transient fields are skipped. A summary of all field kinds is in the section How deserialization works.

Do I have to change serialVersionUID after adding a new field to the class?

No. Adding a field is a compatible change: old data is still read, and the new field gets its default value. Change the number only when you deliberately want to forbid reading old data. Details are in the section serialVersionUID and class versions.

Can I serialize an ArrayList or a HashMap?

Yes, the standard classes of the Collections Framework implement Serializable. But all their elements must be serializable too (for a HashMap, both keys and values), otherwise writing ends with NotSerializableException. You can declare the field as List, but make sure the actual implementation is serializable.

How do I make a deep copy of an object with serialization?

Write the object to a ByteArrayOutputStream through an ObjectOutputStream and immediately read it from a ByteArrayInputStream through an ObjectInputStream: you get an independent copy of the whole object graph. The approach is simple but slow, and every object in the graph must be serializable.

Is built-in Java serialization used in new projects?

Rarely. For data exchange and storage, teams usually pick JSON or Protobuf: they are faster, readable by other languages and safer. Built-in serialization remains in legacy code, RMI, some caches and HTTP sessions, and how it works is a regular interview topic.

Comments

Please log in or register to have a possibility to add comment.