In a previous article Everything You Need to Know About Java Serialization, we discussed how serializability of a class is enabled by implementing the Serializable interface. If our class does not implement Serializable interface or if it is having a reference to a non Serializable class then JVM will throw NotSerializableException.

All subtypes of a serializable class are themselves serializable and Externalizable interface also extends Serializable. So even if we customize our serialization process using Externalizable our class is still a Serializable.

The Serializable interface is a marker interface which has no methods or fields and it works like a flag for the JVM. The Java serialization process provided by ObjectInputStream and ObjectOutputStream classes are fully controlled by the JVM.

But what if we want to add some additional logic to enhance this normal process, for example, we may want to encrypt/decrypt our sensitive information before serializing/deserializing it. Java provides us with some additional methods for this purpose which we are going to discuss in this blog.


Java-Serialization-Magic-Methods-And-Their-Uses

writeObject and readObject methods

Serializable classes that want to customize or add some additional logic to enhance the normal serialization/deserialization process should provide writeObject and readObject methods with these exact signatures:
  • private void writeObject(java.io.ObjectOutputStream out) throws IOException
  • private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException
These methods are already discussed in great details under article Everything You Need to Know About Java Serialization.

readObjectNoData method

As described in Java docs of Serializable class, if we want to initialize the state of the object for its particular class in the event that the serialization stream does not list the given class as a superclass of the object being deserialized then we should provide writeObject and readObject methods with these exact signatures:
  • private void readObjectNoData() throws ObjectStreamException
This may occur in cases where the receiving party uses a different version of the deserialized instance's class than the sending party, and the receiver's version extends classes that are not extended by the sender's version. This may also occur if the serialization stream has tampered; hence, readObjectNoData is useful for initializing deserialized objects properly despite a "hostile" or incomplete source stream.

Each serializable class may define its own readObjectNoData method. If a serializable class does not define a readObjectNoData method, then in the circumstances listed above the fields of the class will be initialized to their default values.

writeReplace and readResolve methods

Serializable classes that need to designate an alternative object to be used when writing an object to the stream should provide this special method with the exact signature:
  • ANY-ACCESS-MODIFIER Object writeReplace() throws ObjectStreamException
And Serializable classes that need to designate a replacement when an instance of it is read from the stream should provide this special method with the exact signature:
  • ANY-ACCESS-MODIFIER Object readResolve() throws ObjectStreamException
Basically, the writeReplace method allows the developer to provide a replacement object that will be serialized instead of the original one. And the readResolve method is used during deserialization process to replace the de-serialized object by another one of our choices.

One of the main usages of writeReplace and readResolve methods is to implement the singleton design pattern with Serialized classes. We know that the deserialization process creates a new object every time and it can also be used as a method to deeply clone an object, which is not good if we have to make our class singleton.

You can read more about Java cloning and serialization on Java Cloning and Java Serialization topics.

The method readResolve is called after readObject has returned (conversely writeReplace is called before writeObject and probably on a different object). The object the method returns replaces this object returned to the user of ObjectInputStream.readObject and any further back-references to the object in the stream. We can use the writeReplace method to replace serializing object with null so nothing will be serialized and then use the readResolve method to replace the deserialized object with the singleton instance.

validateObject method

If we want to perform certain validations on some of our fields we can do that by implementing ObjectInputValidation interface and overriding validateObject method from it.

Method validateObject will automatically get called when we register this validation by calling ObjectInputStream.registerValidation(this, 0) from readObject method. It is very useful to verify the stream has not been tampered with, or that the data makes sense before handing it back to your application.

Below example covers code for all above methods

public class SerializationMethodsExample {

    public static void main(String[] args) throws IOException, ClassNotFoundException {
        Employee emp = new Employee("Naresh Joshi", 25);
        System.out.println("Object before serialization: " + emp.toString());

        // Serialization
        serialize(emp);

        // Deserialization
        Employee deserialisedEmp = deserialize();
        System.out.println("Object after deserialization: " + deserialisedEmp.toString());


        System.out.println();

        // This will print false because both object are separate
        System.out.println(emp == deserialisedEmp);

        System.out.println();

        // This will print false because both `deserialisedEmp` and `emp` are pointing to same object,
        // Because we replaced de-serializing object in readResolve method by current instance
        System.out.println(Objects.equals(emp, deserialisedEmp));
    }

    // Serialization code
    static void serialize(Employee empObj) throws IOException {
        try (FileOutputStream fos = new FileOutputStream("data.obj");
             ObjectOutputStream oos = new ObjectOutputStream(fos))
        {
            oos.writeObject(empObj);
        }
    }

    // Deserialization code
    static Employee deserialize() throws IOException, ClassNotFoundException {
        try (FileInputStream fis = new FileInputStream("data.obj");
             ObjectInputStream ois = new ObjectInputStream(fis))
        {
            return (Employee) ois.readObject();
        }
    }
}

class Employee implements Serializable, ObjectInputValidation {
    private static final long serialVersionUID = 2L;

    private String name;
    private int age;

    public Employee(String name, int age) {
        this.name = name;
        this.age = age;
    }

    // With ObjectInputValidation interface we get a validateObject method where we can do our validations.
    @Override
    public void validateObject() {
        System.out.println("Validating age.");

        if (age < 18 || age > 70)
        {
            throw new IllegalArgumentException("Not a valid age to create an employee");
        }
    }

    // Custom serialization logic,
    // This will allow us to have additional serialization logic on top of the default one e.g. encrypting object before serialization.
    private void writeObject(ObjectOutputStream oos) throws IOException {
        System.out.println("Custom serialization logic invoked.");
        oos.defaultWriteObject(); // Calling the default serialization logic
    }

    // Replacing de-serializing object with this,
    private Object writeReplace() throws ObjectStreamException {
        System.out.println("Replacing serialising object by this.");
        return this;
    }

    // Custom deserialization logic
    // This will allow us to have additional deserialization logic on top of the default one e.g. performing validations, decrypting object after deserialization.
    private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
        System.out.println("Custom deserialization logic invoked.");

        ois.registerValidation(this, 0); // Registering validations, So our validateObject method can be called.

        ois.defaultReadObject(); // Calling the default deserialization logic.
    }

    // Replacing de-serializing object with this,
    // It will will not give us a full proof singleton but it will stop new object creation by deserialization.
    private Object readResolve() throws ObjectStreamException {
        System.out.println("Replacing de-serializing object by this.");
        return this;
    }

    @Override
    public String toString() {
        return String.format("Employee {name='%s', age='%s'}", name, age);
    }
}

You can find the complete source code for this article on this Github Repository and please feel free to provide your valuable feedback.
In my previous articles, I had explained the difference between deep and shallow cloning and how copy-constructors and defensive copy methods are better than default java cloning.

Java object cloning using copy constructors and defensive copy methods certainly have some advantages but we have to explicitly write some code to achieve deep cloning in all these approaches. And still, there are chances that we might miss something and do not get deeply cloned object.

And as discussed in 5 different ways to create objects in java, deserialising a serialised object creates a new object with the same state as in the serialized object. So similar to above cloning approaches we can achieve deep cloning functionality using object serialization and deserialization as well and with this approach we do not have worry about or write code for deep cloning, we get it by default.

However, cloning an object using serialization comes with some performance overhead and we can improve on it by using in-memory serialization if we just need to clone the object and don’t need to persist it in a file for future use.

We will use below Employee class as an example which has name, doj and skills as the state, for deep cloning, we do not need to worry about the name field because it is a String object and by default all strings are immutable in nature.

You can read more about immutability on How to Create an Immutable Class in Java and Why String is Immutable and Final.

class Employee implements Serializable {
    private static final long serialVersionUID = 2L;

    private String name;
    private LocalDate doj;
    private List<String> skills;

    public Employee(String name, LocalDate doj, List<String> skills) {
        this.name = name;
        this.doj = doj;
        this.skills = skills;
    }

    public String getName() { return name; }
    public LocalDate getDoj() { return doj; }
    public List<String> getSkills() { return skills; }

    // Method to deep clone a object using in memory serialization
    public Employee deepClone() throws IOException, ClassNotFoundException {
        // First serializing the object and its state to memory using ByteArrayOutputStream instead of FileOutputStream.
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        ObjectOutputStream out = new ObjectOutputStream(bos);
        out.writeObject(this);

        // And then deserializing it from memory using ByteArrayOutputStream instead of FileInputStream.
        // Deserialization process will create a new object with the same state as in the serialized object,
        ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
        ObjectInputStream in = new ObjectInputStream(bis);
        return (Employee) in.readObject();
    }

    @Override
    public String toString() {
        return String.format("Employee{name='%s', doj=%s, skills=%s}", name, doj, skills);
    }

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

        Employee employee = (Employee) o;

        return Objects.equals(name, employee.name) &&
            Objects.equals(doj, employee.doj) &&
            Objects.equals(skills, employee.skills);
    }

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


To deep clone an object of Employee class, I have provided a deepClone() method which serializes the object to memory by using ByteArrayOutputStream instead of the FileOutputStream and deserialises it back using ByteArrayInputStream instead of the FileInputStream. Here we are serializing the object into bytes and deserializing it from bytes to object again.

How To Deep Clone An Object Using Java In Memory Serialization



Employee class implements Serializable interface to achieve serialization which has its own disadvantages and we can overcome some of these disadvantages by customizing the serialization process by using Externalizable interface.

We can run below tests to see if our cloning approach is deep or just shallow, here all == operations will return false (because both objects are separate) and all equals will return true (because both have the same content).

public static void main(String[] args) throws IOException, ClassNotFoundException {
 Employee emp = new Employee("Naresh Joshi", LocalDate.now(), Arrays.asList("Java", "Scala", "Spring"));
 System.out.println("Employee object: " + emp);

 // Deep cloning `emp` object by using our `deepClone` method.
 Employee clonedEmp = emp.deepClone();
 System.out.println("Cloned employee object: " + clonedEmp);

 System.out.println();

 // All of this will print false because both objects are separate.
 System.out.println(emp == clonedEmp);
 System.out.println(emp.getDoj() == clonedEmp.getDoj());
 System.out.println(emp.getSkills() == clonedEmp.getSkills());

 System.out.println();

 // All of this will print true because `clonedEmp` is a deep clone of `emp` and both have the same content.
 System.out.println(Objects.equals(emp, clonedEmp));
 System.out.println(Objects.equals(emp.getDoj(), clonedEmp.getDoj()));
 System.out.println(Objects.equals(emp.getSkills(), clonedEmp.getSkills()));
}

We know the deserialization process creates a new object every time which is not good if we have to make our class singleton. And that's why we need to override and disable serialization for our singleton class which we can achieve by providing writeReplace and readResolve methods.

Similar to serialization, Java cloning also does not play along with singleton pattern, and that's why we need to override and disable it as well. We can do that by implementing cloning in a way so that it will either throw CloneNotSupportedException or return the same instance every time.

You can read more about Java cloning and serialization on Java Cloning and Java Serialization topics.

You can find the complete source code for this article on this Github Repository and please feel free to provide your valuable feedback.
In a previous article Everything About Java Serialization Explained With Example, I explained how we can serialize/deserialize one object using Serializable interface and also explain how we can customise the serialization process using writeObject and readObject methods.

Disadvantages Of Java Serialization Process

But these customizations are not sufficient because JVM has full control of the serialization process and those customization logics are just additions to the default serialization process. We still have to use the default serialization logic by calling ObjectOutputStream.defaultWriteObject() and ObjectInputStream.defaultReadObject() from writeObject and readObject methods. And if do not call these default methods our object will not be serialized/deserialized.

The default serialization process is fully recursive. So whenever we try to serialize one object, the serialization process try to serialize all the fields (primitive and reference) with our class (except static and transient fields). Which makes serialization a very slow process.

Now let's assume we have an object with lots of fields which we do not want to serialize for some reasons (these fields will always be assigned with default values). With default serialization process we will have to make all these fields transient but it still will not be efficient because there will be a lot of checks to see if the fields are transient or not.

So as we can see there are lots of downside of the using default serialization process, like:
  1. Customizations to serialization are not sufficient because JVM has full control of the serialization process and our customization logics are just additions to the default serialization process.
  2. Default serialization process is fully recursive and slow.
  3. In order to not to serialize a field, we have to declare it transient and lots of transient fields will again make the process slow.
  4. We can not control how our fields will be serialized and deserialized.
  5. Default serialization process does not invoke constructors while creating the object so it can not call the initialization logic provided by the constructor.

What Is Externalization And Externalizable Interface


As we saw above that the default java serialization is not efficient. We can solve some of these issues by using Externalizable interface instead of Serializable interface.

We can write your own serialization logic by implementing the Externalizable interface and overriding it’s methods writeExternal() and readExternal(). But with this approach, we will not get any kind of default serialization logic from JVM and it is up to us to provide the complete serialization and deserialization logic.

So it is very necessary to code the test these methods very carefully because it might break the serialization process. But the externalization process is very fast in comparison to the default serialization process if implemented properly.

We will use below Employee class object as an example for the explanation:

// Using Externalizable, complete serialization/deserialization logic becomes our responsibility,
// We need to tell what to serialize using writeExternal() method and what to deserialize using readExternal(),
// We can even serialize/deserialize static and transient variables,
// With implementation of writeExternal() and readExternal(),  methods writeObject() and readObject() becomes redundant and they do not get called.
class Employee implements Externalizable {

   // This serialVersionUID field is necessary for Serializable as well as Externalizable to provide version control,
    // Compiler will provide this field if we do not provide it which might change if we modify class structure of our class, and we will get InvalidClassException,
    // If we provide a value to this field and do not change it, serialization-deserialization will not fail if we change our class structure.
    private static final long serialVersionUID = 2L;

    private String firstName;
    private transient String lastName; // Using Externalizable, we can even serialize/deserialize transient variables, so declaring fields transient becomes unnecessary.
    private int age;
    private static String department; // Using Externalizable, we can even serialize/deserialize static variables according to our need.


    // Mandatory to have to make our class Externalizable
    // When an Externalizable object is reconstructed, the object is created using public no-arg constructor before the readExternal method is called.
    // If a public no-arg constructor is not present then a InvalidClassException is thrown at runtime.
    public Employee() {
    }

    // All-arg constructor to create objects manually
    public Employee(String firstName, String lastName, int age, String department) {
        this.firstName = firstName;
        this.lastName = lastName;
        this.age = age;
        Employee.department = department;

        validateAge();
    }

    private void validateAge() {
        System.out.println("Validating age.");

        if (age < 18 || age > 70) {
            throw new IllegalArgumentException("Not a valid age to create an employee");
        }
    }

    @Override
    // We need to tell what to serialize in writeExternal() method
    public void writeExternal(ObjectOutput out) throws IOException {
        System.out.println("Custom externalizable serialization logic invoked.");

        out.writeUTF(firstName);
        out.writeUTF(lastName);
        out.writeInt(age);
        out.writeUTF(department);
    }

    @Override
    // We need to tell what to deserialize in readExternal() method
    // The readExternal method must read the values in the same sequence and with the same types as were written by writeExternal
    public void readExternal(ObjectInput in) throws IOException {
        System.out.println("Custom externalizable serialization logic invoked.");

        firstName = in.readUTF();
        lastName = in.readUTF();
        age = in.readInt();
        department = in.readUTF();

        validateAge();
    }

    @Override
    public String toString() {
        return String.format("Employee {firstName='%s', lastName='%s', age='%s', department='%s'}", firstName, lastName, age, department);
    }

    // Custom serialization logic, It will be called only if we have implemented Serializable instead of Externalizable.
    private void writeObject(ObjectOutputStream oos) throws IOException {
        System.out.println("Custom serialization logic invoked.");
    }

    // Custom deserialization logic, It will be called only if we have implemented Serializable instead of Externalizable.
    private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
        System.out.println("Custom deserialization logic invoked.");
    }
}

How Serialization works with Externalizable Interface

As we can see above in our example Employee class, we can write your own serialization logic by implementing the Externalizable interface and overriding its methods writeExternal() and readExternal().

The object can implement the writeExternal method to save its contents by calling the methods of DataOutput for its primitive values or calling the writeObject method of ObjectOutput for objects, strings, and arrays.

The object can implement the readExternal method to restore its contents by calling the methods of DataInput for primitive types and readObject for objects, strings and arrays. The readExternal method must read the values in the same sequence and with the same types as were written by writeExternal.

// We need to tell what fields to serialize in writeExternal() method
public void writeExternal(ObjectOutput out) throws IOException {
    System.out.println("Custom externalizable serialization logic invoked.");

    out.writeUTF(firstName);
    out.writeUTF(lastName);
    out.writeInt(age);
    out.writeUTF(department);
}

// We need to tell what fields to deserialize in readExternal() method
// The readExternal method must read the values in the same sequence and with the same types as were written by writeExternal
public void readExternal(ObjectInput in) throws IOException {
    System.out.println("Custom externalizable serialization logic invoked.");

    firstName = in.readUTF();
    lastName = in.readUTF();
    age = in.readInt();
    department = in.readUTF();

    validateAge();
}

To serialize and deserialize our object to a file we need to follow the same procedure as we followed in the Serializable example which means calling ObjectOutputStream.writeObject() and ObjectInputStream.readObject() as done in the following code:

public class ExternalizableExample {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        Employee empObj = new Employee("Shanti", "Sharma", 25, "IT");
        System.out.println("Object before serialization  => " + empObj.toString());

        // Serialization
        serialize(empObj);

        // Deserialization
        Employee deserializedEmpObj = deserialize();
        System.out.println("Object after deserialization => " + deserializedEmpObj.toString());
    }

    // Serialization code
    static void serialize(Employee empObj) throws IOException {
        try (FileOutputStream fos = new FileOutputStream("data.obj");
             ObjectOutputStream oos = new ObjectOutputStream(fos))
        {
            oos.writeObject(empObj);
        }
    }

    // Deserialization code
    static Employee deserialize() throws IOException, ClassNotFoundException {
        try (FileInputStream fis = new FileInputStream("data.obj");
             ObjectInputStream ois = new ObjectInputStream(fis))
        {
            return (Employee) ois.readObject();
        }
    }
}

The Externalizable interface is a child interface of Serializable i.e. Externalizable extends Serializable. So if we implement Externalizable interface and override its writeExternal() and readExternal() methods then first preference is given to these methods over the default serialization mechanism provided by JVM. These methods supersede customized implementations of writeObject and readObject methods, So if we also provide writeObject() and readObject() then they will be ignored.

In the serialization process, each object to be serialized is tested for the Externalizable interface. If the object supports Externalizable, the writeExternal method is called. If the object does not support Externalizable and does implement Serializable, the object is saved using ObjectOutputStream.

When an Externalizable object is reconstructed, an instance is created using the public no-arg constructor, then the readExternal method called. Serializable objects are restored by reading them from an ObjectInputStream.

  1. When an Externizable object is reconstructed, and object is created using public no-arg constructor before the readExternal method is called. If a public no-arg constructor is not present then a InvalidClassException is thrown at runtime.
  2. Using Externalizable, we can even serialize/deserialize transient variables, so declaring fields transient becomes unnecessary.
  3. Using Externalizable, we can even serialize/deserialize static variables if we need to.

An Externalizable instance can designate a substitution object via the writeReplace and readResolve methods documented in the Serializable interface.

Java serialization can also be used to deep clone an object. Java cloning is the most debatable topic in Java community and it surely does have its drawbacks but it is still the most popular and easy way of creating a copy of an object until that object is full filling mandatory conditions of Java cloning. I have covered cloning in details in a 3 article long Java Cloning Series which includes articles like Java Cloning And Types Of Cloning (Shallow And Deep) In Details With Example, Java Cloning - Copy Constructor Versus Cloning, Java Cloning - Even Copy Constructors Are Not Sufficient, go ahead and read them if you want to know more about cloning.

Differences between Externalizable vs Serializable

Let’s list down the main differences between Externalizable and Serializable interfaces in java.




You can find the complete source code for this article on this Github Repository and please feel free to provide your valuable feedback.
In a previous article, we looked at 5 different ways to create objects in java, I have explained how deserialising a serialised object creates a new object and in this blog, I am going to discuss Serialization and Deserialization in details.

We will use below Employee class object as an example for the explanation

// If we use Serializable interface, static and transient variables do not get serialize
class Employee implements Serializable {

    // This serialVersionUID field is necessary for Serializable as well as Externalizable to provide version control,
    // Compiler will provide this field if we do not provide it which might change if we modify the class structure of our class, and we will get InvalidClassException,
    // If we provide value to this field and do not change it, serialization-deserialization will not fail if we change our class structure.
    private static final long serialVersionUID = 2L;

    private final String firstName; // Serialization process do not invoke the constructor but it can assign values to final fields
    private transient String middleName; // transient variables will not be serialized, serialised object holds null
    private String lastName;
    private int age;
    private static String department; // static variables will not be serialized, serialised object holds null

    public Employee(String firstName, String middleName, String lastName, int age, String department) {
        this.firstName = firstName;
        this.middleName = middleName;
        this.lastName = lastName;
        this.age = age;
        Employee.department = department;

        validateAge();
    }

    private void validateAge() {
        System.out.println("Validating age.");

        if (age < 18 || age > 70) {
            throw new IllegalArgumentException("Not a valid age to create an employee");
        }
    }

    @Override
    public String toString() {
        return String.format("Employee {firstName='%s', middleName='%s', lastName='%s', age='%s', department='%s'}", firstName, middleName, lastName, age, department);
    }

  // Custom serialization logic,
    // This will allow us to have additional serialization logic on top of the default one e.g. encrypting object before serialization
    private void writeObject(ObjectOutputStream oos) throws IOException {
        System.out.println("Custom serialization logic invoked.");
        oos.defaultWriteObject(); // Calling the default serialization logic
    }

    // Custom deserialization logic
    // This will allow us to have additional deserialization logic on top of the default one e.g. decrypting object after deserialization
    private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
        System.out.println("Custom deserialization logic invoked.");

        ois.defaultReadObject(); // Calling the default deserialization logic

        // Age validation is just an example but there might some scenario where we might need to write some custom deserialization logic
        validateAge();
    }

}

What are Serialization and Deserialization

In Java, we create several objects which live and die accordingly and every object will certainly die when the JVM dies but sometimes we might want to reuse an object between several JVMs or we might want to transfer an object to another machine over the network.

Well, serialization allows us to convert the state of an object into a byte stream, which then can be saved into a file on the local disk or sent over the network to any other machine. And deserialization allows us to reverse the process, which means reconverting the serialized byte stream to an object again.

In simple words, object serialization is the process of saving an object's state to a sequence of bytes and deserialization is the process of reconstructing an object from those bytes. Generally, the complete process is called serialization but I think it is better to classify both as separate for more clarity.

The serialization process is platform independent, an object serialized on one platform can be deserialized on a different platform.

To serialize and deserialize our object to a file we need to call ObjectOutputStream.writeObject() and ObjectInputStream.readObject() as done in the following code:

public class SerializationExample {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        Employee empObj = new Employee("Shanti", "Prasad", "Sharma", 25, "IT");
        System.out.println("Object before serialization  => " + empObj.toString());

        // Serialization
        serialize(empObj);

        // Deserialization
        Employee deserialisedEmpObj = deserialize();
        System.out.println("Object after deserialization => " + deserialisedEmpObj.toString());
    }

    // Serialization code
    static void serialize(Employee empObj) throws IOException {
        try (FileOutputStream fos = new FileOutputStream("data.obj");
             ObjectOutputStream oos = new ObjectOutputStream(fos))
        {
            oos.writeObject(empObj);
        }
    }

    // Deserialization code
    static Employee deserialize() throws IOException, ClassNotFoundException {
        try (FileInputStream fis = new FileInputStream("data.obj");
             ObjectInputStream ois = new ObjectInputStream(fis))
        {
            return (Employee) ois.readObject();
        }
    }
}

Only classes which implement Serializable can be serialized

Similar to the Cloneable interface for Java cloning in serialization, we have one marker interface Serializable which works like a flag for the JVM. Any class which implements Serializable interface directly or through its parent can be serialised and classes which do not implement Serializable can not be serialized.

Java’s default serialization process is fully recursive, so whenever we try to serialize one object, the serialization process try to serialize all the fields (primitive and reference) with our class (except static and transient fields).

When a class implements theSerializable interface, all its sub-classes are serializable as well. But when an object has a reference to another object, these objects must implement the Serializable interface separately. If our class is having even a single reference to a non Serializable class then JVM will throw NotSerializableException.

Why Serializable is not implemented by Object?

Now a question arises if Serialization is very basic functionality and any class which do not implement Serializable can not be serialised, then why Serializable is not implemented by the Object itself?, By this way, all our objects could be serialized by default.

The Object class does not implement Serializable interface because we may not want to serialize all the objects e.g. serialising a thread does not make any sense because thread running in my JVM would be using my system's memory, persisting it and trying to run it in your JVM would make no sense.

The transient and static fields do not get serialized

If we want to serialize one object but do not want to serialize some specific fields then we can mark those fields as transient.

All the static fields belong to the class instead of the object, and the serialization process serialises the object so static fields can not be serialized.

  1. Serialization does not care about access modifiers of the field such as private. All non transient and non static fields are considered part of an object's persistent state and are eligible for serialisation.
  2. We can assign values to final fields in conscrutors only and serialization process do not invoke any constructor but still it can assign values to final fields.

What is serialVersionUID and Why should we declare it?

Suppose we have a class and we have serialized its object to a file on the disk, and due to some new requirements, we added/removed one field from our class. Now if we try to deserialize the already serialized object we will get InvalidClassException, why?

We get it because by default JVM associates a version number to each serializable class to control the class versioning. It is used to verify that the serialized and deserialized objects have the same attributes and thus are compatible with deserialization. The version number is maintained in a field called serialVersionUID. If a serializable class doesn’t declare a serialVersionUID JVM will generate one automatically at run-time.

If we change our class structure e.g. remove/add fields that version number also changes and according to JVM our class is not compatible with the class version of the serialized object. That's why we get the exception but if you really think about it, why should it be thrown just because I added a field? Couldn't the field just be set to its default value and then written out next time?

Yes, it can be done by providing the serialVersionUID field manually and ensure it is always the same. It is highly recommended that each serializable class declares its serialVersionUID as the generated one is compiler dependent and thus may result in unexpected InvalidClassExceptions.


You can use a utility that comes with the JDK distribution called serialver to see what that code would be by default (it is just the hash code of the object by default).

Customizing Serialization and Deserialization with writeObject and readObject methods

JVM has full control for serializing the object in the default serialization process but there are lots of downside of the using default serialization process, some of which are:

  1. It can not handle serialisation of fields which are not serializable.
  2. Deserialization process does not invoke constructors while creating the object so it can not call the initialization logic provided by the constructor.

But we can override this the default serialization behaviour inside our Java class and provide some additional logic to enhance the normal process. This can be done by providing two methods writeObject and readObject inside the class that we want to serialize:

// Custom serialization logic will allow us to have additional serialization logic on top of the default one e.g. encrypting object before serialization
private void writeObject(ObjectOutputStream oos) throws IOException {
  // Any Custom logic
 oos.defaultWriteObject(); // Calling the default serialization logic
  // Any Custom logic
}

// Custom deserialization logic will allow us to have additional deserialization logic on top of the default one e.g. decrypting object after deserialization
private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
 // Any Custom logic
 ois.defaultReadObject(); // Calling the default deserialization logic
  // Any Custom logic
}

Declaring both methods as private is necessary (public methods will not work) so rather than JVM nothing else can see them. This also proves that neither method is not inherited nor overridden or overloaded. JVM automatically checks these methods and call them during the serialization-deserialization process. JVM can call these private methods but other objects can not thus, the integrity of the class is maintained and the serialization protocol can continue to work as normal.

Even though those specialized private methods are provided, the object serialization works the same way by calling ObjectOutputStream.writeObject() or ObjectInputStream.readObject().

The call to ObjectOutputStream.writeObject() or ObjectInputStream.readObject() kicks off the serialization protocol. First, the object is checked to ensure it implements Serializable and then it is checked to see whether either of those private methods is provided. If they are provided, the stream class is passed as the parameter to these methods, giving the code control over its usage.

We can call ObjectOutputStream.defaultWriteObject() and ObjectInputStream.defaultReadObject() from these methods to gain default serialization logic. Those calls do what they sound like -- they perform the default writing and reading of the serialized object, which is important because we are not replacing the normal process, we are only adding to it.

Those private methods can be used for any customization you want to make in the serialization process, e.g. encryption can be added to the output and decryption to the input (note that the bytes are written and read in cleartext with no obfuscation at all). They could be used to add extra data to the stream, perhaps a company versioning code, the possibilities are truly limitless.

Apart from writeObject and readObject methods, we can also leverage other serialization magic methods to get some additional functionality.

Stopping Serialization and Deserialization

Suppose we have a class which got the serialization capability from its parent, which means our class extends from another class which implements Serializable.

It means anybody can serialize and deserialize the object of our class. But what if we do not want our class to be serialized or deserialized e.g. our class is a singleton and we want to prevent any new object creation, remember the deserialization process creates a new object.

To stop the serialization for our class, we can once again use the above private methods to just throw the NotSerializableException. Any attempt to serialize or deserialise our object will now always result in the exception being thrown. And since those methods are declared as private, nobody can override your methods and change them.

private void writeObject(ObjectOutputStream oos) throws IOException {
  throw new NotSerializableException("Serialization is not supported on this object!");
}

private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
  throw new NotSerializableException("Serialization is not supported on this object!");
}


However, this is a violation of the Liskov Substitution Principle. And writeReplace and readResolve methods can be used to achieve singleton like behaviours. These methods are used to allow an object to provide an alternative representation for itself within an ObjectStream. In simple words, readResolve can be used to change the data that is deserialized through the readObject method and writeReplace can be used to change the data that is serialized through writeObject.

Java serialization can also be used to deep clone an object. Java cloning is the most debatable topic in Java community and it surely does have its drawbacks but it is still the most popular and easy way of creating a copy of an object until that object is full filling mandatory conditions of Java cloning. I have covered cloning in details in a 3 article long Java Cloning Series which includes articles like Java Cloning And Types Of Cloning (Shallow And Deep) In Details With Example, Java Cloning - Copy Constructor Versus Cloning, Java Cloning - Even Copy Constructors Are Not Sufficient, go ahead and read them if you want to know more about cloning.

Conclusion

  1. Serialization is the process of saving an object's state to a sequence of bytes which then can be stored on a file or sent over the network and deserialization is the process of reconstructing an object from those bytes.
  2. Only subclasses of the Serializable interface can be serialized.
  3. If our class does not implement Serializable interface or if it is having a reference to a non Serializable class then JVM will throw NotSerializableException.
  4. All transient and static fields do not get serialized.
  5. The serialVersionUID is used to verify that the serialized and deserialized objects have the same attributes and thus are compatible with deserialization.
  6. We should create a serialVersionUID field in our class so if we change our class structure (adding/removing fields) JVM will not through InvalidClassException. If we do not provide it JVM provides one which might change when our class structure changes.
  7. We can override the default serialization behaviour inside our Java class by providing the implementation of writeObject and readObject methods.
  8. And we can call ObjectOutputStream.defaultWriteObject() and ObjectInputStream.defaultReadObject from writeObject and readObject methods to get the default serialization and deserialization logic.
  9. We can throw NotSerializableException exception from writeObject and readObject , if we do not want our class to be serialized or deserialized.
Java Serialization process can be further customized and enhanced using the Externalizable interface which I have explained in How to Customize Serialization In Java By Using Externalizable Interface.

I have also written a series of articles explaining item numbers 74 to 78 of Effective Java, which further discusses how Java serialization process can be enhanced, please go ahead and read them if you like.

You can find the complete source code for this article on this Github Repository and please feel free to provide your valuable feedback.

Next Post Newer Posts Previous Post Older Posts Home