Why Do Remote Java Transmission Objects Need to Be Serialized?

1. Introduction

Serialization and deserialization are tasks that java engineers deal with almost every day, especially in today’s popular microservices development. 

Just looking at the definition, it might be hard for beginners to immediately grasp the significance of serialization—especially when faced with such academic terminology. Naturally, one might wonder: What exactly is it, and what is it used for?

From a programming perspective, we know that the smallest unit of data exchanged between computers is the byte stream. Serialization is the process of converting an object into a byte stream that any computer can recognize, while deserialization is converting this byte stream back into an object that a program can understand. Simply put, the ultimate purpose of serialization is to make objects easier to store across platforms and transmit over networks.

Basically, any data that involves cross-platform storage or network transmission needs to be serialized. In the early days of the internet, serialization methods primarily included COM and CORBA. COM was mainly used on Windows platforms and did not truly support cross-platform functionality. Moreover, COM serialization relied on the use of virtual tables in compilers, making it difficult to learn (imagine java engineers wanting a simple serialization protocol, but first needing to master compiler internals). Also, since the serialized data was tightly coupled with the compiler, it was hard to extend with additional properties.

CORBA, on the other hand, was one of the earliest protocols to support cross-platform and cross-language serialization. However, its major drawbacks included too many stakeholders, too many versions, poor compatibility between versions, and overly complex usage. These issues—political, economic, technical, and stemming from immature early design—ultimately led to CORBA’s failure.

After J2SE 1.3, Java introduced RMI-IIOP technology based on the CORBA protocol, allowing Java developers to build CORBA applications entirely in Java. With the rapid advancement of software technologies, newer and more popular serialization methods gradually emerged, such as XML, JSON, Protobuf, Thrift, and Avro. Each of these serialization methods has its own strengths; there is no single "best" option. Instead, the most suitable method depends on your environment.

If you're selecting a serialization technology for your project, consider the following factors:

  • Cross-platform support: Especially important for projects developed in multiple languages; cross-platform compatibility directly affects development complexity.

  • Serialization speed: Faster methods can significantly improve system performance.

  • Serialized data size: Smaller data is better—it transmits faster, consumes less bandwidth, and improves overall system performance.

As a Java developer, how should we use serialization, and what should we pay attention to during the serialization process? Let’s go through into the next.

2. Practical Serialization Code Example

Implementing serialization in Java is very simple—just implement the Serializable interface, as shown in the following class:

public class Student implements Serializable {

    /**
     * Username
     */
    private String name;

    /**
     * Age
     */
    private Integer age;

    public Student(String name, Integer age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public String toString() {
        return "Student{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}


Let’s do a test: we’ll serialize a Student object into binary data and store it in a file, then read the data from the file and convert it back into a Student object. This process is basically serialization and deserialization.

public class RemoteObjectTest {

    public static void main(String[] args) throws Exception {
        // Serialization
        serializeStudent();
        // Deserialization
        deserializeStudent();
    }

    private static void serializeStudent() throws Exception {
        Student black = new Student("David Brat", 40);
        System.out.println(black.toString());
        System.out.println("======== Serialization Started =========");
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("remote_object.log"));
        oos.writeObject(black);
        oos.flush();
        oos.close();
    }

    private static void deserializeStudent() throws Exception {
        System.out.println("======== Deserialization Started ========");
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("remote_object.log"));
        Student black = (Student) ois.readObject();
        ois.close();
        System.out.println(black.toString());
    }
}
Output:
Student{name='David Brat', age=40}
======== Serialization Started =========
======== Deserialization Started ========
Student{name='David Brat', age=40}

Looks super simple, doesn’t it? But don’t be careless—there are quite a few hidden pitfalls. Check out the issues summarized below!

3. Pitfalls of Serialization Applications

3.1 Static Fields Are Not Serialized

Static fields are not included in serialization. Because static variables belong to the class state, serialization does not save them.

3.2 Transient Fields Are Not Serialized

Fields marked with transient cannot be serialized. To demonstrate, we add a transient modifier to the name field in the Student class:

public class Student implements Serializable {

    /**
     * User name
     */
    private transient String name;

    // ... other fields and methods omitted
}

After running the test, the output is as follows:

Student{name='null', age=40}

As you can see, the name field—being marked transient—is null after deserialization.

3.3 Serialization Version UID (serialVersionUID) Issues

Any class that implements Serializable has a version identifier. If we don’t explicitly define one, the JDK will generate a UID based on the class’s fields. We can also define our own. For example, to give the Student class a custom version UID, do the following:

public class Student implements Serializable {

    // Custom serialization version UID
    private static final long serialVersionUID = 10L;

    // ... other fields and methods omitted
}

How can we verify this?

First, serialize a Student object without a custom serialVersionUID.

Then, add a serialVersionUID = 10L to the class definition and attempt to deserialize the previously serialized data.

Exception in thread "main" java.io.InvalidClassException: com.example.java.serializable.test1.entity.Student; local class incompatible: stream classdesc serialVersionUID = 854479923312419517, local class serialVersionUID = 10
    at java.io.ObjectStreamClass.initNonProxy(ObjectStreamClass.java:702)
    at java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:1891)
    at java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1756)
    at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2048)
    at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1581)
    at java.io.ObjectInputStream.readObject(ObjectInputStream.java:436)

The result is a deserialization failure. 

Why? When the Student object was serialized, the automatically generated UID was something like 854479923312419517. During deserialization, the class’s UID is now 10L. Because these values don’t match, Java throws an InvalidClassException.

So if you don’t explicitly declare serialVersionUID, the JDK computes one from the class’s field structure. As long as the fields remain unchanged, the generated UID usually stays the same—but any change to the class (adding/removing fields, changing field types, etc.) will change the generated UID. If serialized data was produced under a different UID than the current class definition, deserialization will fail.

In development, it’s inevitable that an entity class’s fields will change. Some developers implement the Serializable interface without defining a version UID. We strongly recommend that you always declare a custom serialVersionUID for every class that implements Serializable. That way, even if the class’s fields change, it won’t affect serialization or deserialization of existing data. 

It’s very simple—just add this static variable to your class:

// Custom serialization version UID by your own
private static final long serialVersionUID = 10L;

3.4 Serialization Issues with Parent and Child Classes

In actual development—especially when dealing with entity classes—we often use inheritance to reuse object fields. But once inheritance is involved, can the parent class's fields still be properly serialized? Let’s explore this!

First, we create two classes: Parent and Child, where Child inherits from ParentParent Class Does NOT Implement Serializable, but Child Class DOES.

public class Child extends Parent {

    private String id;
    public String getId() {
        return id;
    }
    public Child setId(String id) {
        this.id = id;
        return this;
    }

}
public class Child extends Parent implements Serializable{

    private static final long serialVersionUID = 10l;
    private String id;
    public String getId() {
        return id;
    }
    public Child setId(String id) {
        this.id = id;
        return this;
    }
    
}

Then, we write a test class to serialize and then deserialize the object:

public class RemoteObjectTest {

    public static void main(String[] args) throws Exception {
        serializeAnimal();
        deserializeAnimal();
    }

    private static void serializeAnimal() throws Exception {
        Child black = new Child();
        black.setId("1000");
        black.setName("tiger");
        System.out.println("id:" + black.getId() + ", name:" + black.getName());
        System.out.println("======== Start Serialization ========");
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("remote_object.log"));
        oos.writeObject(black);
        oos.flush();
        oos.close();
    }

    private static void deserializeAnimal() throws Exception {
        System.out.println("======== Start Deserialization =========");
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("remote_object.log"));
        Child black = (Child) ois.readObject();
        ois.close();
        System.out.println("id:" + black.getId() + ", name:" + black.getName());
    }
}

Output:

id:1000,name:tiger
======== Start Serialization ======== ======== Start Deserialization ========= id:1000,name:null

As you can clearly see, the parent class’s fields were not serialized.

Now let’s test another common scenario where only the parent class implements Serializable.

public class Parent implements Serializable {

    private static final long serialVersionUID = 10L;
    private String name;
    public String getName() {
        return name;
    }
    public Parent setName(String name) {
        this.name = name;
        return this;
    }

}
public class Child extends Parent {

    private String id;
    public String getId() {
        return id;
    }
    public Child setId(String id) {
        this.id = id;
        return this;
    }

}

After running the same test again, the output is:

id:1000,name:tiger
======== Start Serialization ======== ======== Start Deserialization ========= id:1000,name:tiger
This time, it's clear that the parent class’s fields were serialized.

What if both the parent and child classes implement Serializable, but have different serialVersionUID values?

import java.io.Serializable;

public class Parent implements Serializable {

    private static final long serialVersionUID = 10L;    
    private String name;
    public String getName() {
        return name;
    }
    public Parent setName(String name) {
        this.name = name;
        return this;
    }
}
import java.io.Serializable;

public class Child extends Parent implements Serializable {

    private static final long serialVersionUID = 20L;
    private String id;
    public String getId() {
        return id;
    }
    public Child setId(String id) {
        this.id = id;
        return this;
    }
}

Let’s see what happens when:

The parent class implements Serializable, and the child class also implements Serializable.

After running the program, the result is:

id:1000,name:tiger
======== Start Serialization ======== ======== Start Deserialization ========= id:1000,name:tiger

The parent class's fields are still successfully serialized.

When both the parent and child classes implement Serializable and define different serialVersionUIDs, the serialization process follows the version number of the child class.

So:

  • When the parent class implements Serializable, all properties—including those in the child class—will be serialized.

  • However, when the parent class does not implement Serializable, its properties will not be serialized even if the child class does implement it.

3.5 Custom Serialization Process

The serialization process of the Serializable interface is automatically handled by the JVM.
However, in certain scenarios, you might want to customize the serialization and deserialization behavior without modifying the fields of the entity class. In such cases, you can implement custom serialization manually.

Custom serialization is quite straightforward: you just need to implement the JDK-provided Externalizable interface. This interface defines two core methods — one for writing data and the other for reading data.

public interface Externalizable extends java.io.Serializable {
    void writeExternal(ObjectOutput out) throws IOException;
    void readExternal(ObjectInput in) throws IOException, ClassNotFoundException;
}

The implementation of the Externalizable interface is also simple. Let's create a class named Person and implement the two methods provided by Externalizable.

public class Person implements Externalizable {

    private static final long serialVersionUID = 10L;
    private String name;
    private int age;

    /**
     * When implementing the Externalizable interface, a no-argument constructor must be provided,
     * as it will be invoked during deserialization.
     */
    public Person() {
        System.out.println("Person: empty");
    }

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

    @Override
    public void writeExternal(ObjectOutput out) throws IOException {
        System.out.println("person writeExternal...");
        out.writeObject(name);
        out.writeInt(age);
    }

    @Override
    public void readExternal(ObjectInput in) throws ClassNotFoundException, IOException {
        System.out.println("person readExternal...");
        name = (String) in.readObject();
        age = in.readInt();
    }

    @Override
    public String toString() {
        return "Person{" + "name='" + name + '\'' + ", age=" + age + '}';
    }
}

Test the serialization and deserialization of the Person object.

public class ExternalizableMain {

    public static void main(String[] args) throws IOException, ClassNotFoundException {
        serializable();
        deserializable();
    }

    private static void serializable() throws IOException {
        Person person = new Person("David Brat", 45);
        System.out.println(person.toString());
        System.out.println("=================start serialization================");
        FileOutputStream boas = new FileOutputStream("person_object.log");
        ObjectOutputStream oos = new ObjectOutputStream(boas);
        oos.writeObject(person);
        oos.close();
        boas.close();
    }

    private static void deserializable() throws IOException, ClassNotFoundException {
        System.out.println("============start deserialization=============");
        ObjectInputStream bis = new ObjectInputStream(new FileInputStream("person_object.log"));
        Person person = (Person)bis.readObject();
        System.out.println(person.toString());
    }
}

The output is as follows:

Person{name='David Brat', age=45}
=================start serialization================
person writeExternal...
============start deserialization=============
Person: empty
person readExternal...
Person{name='David Brat', age=45}

4. Summary

Object serialization is very commonly used in real-world java development, especially in microservices. For instance, if you're using a framework combination like Spring Boot + Eureka, then during RPC calls, if the transmitted object hasn't implemented serialization, it will throw an error directly!

When working with serialization, there are quite a few pitfalls—particularly with the version number (serialVersionUID), which is often overlooked. 

In actual development, it is strongly recommended to explicitly define a version number. This helps avoid deserialization errors when the properties of the transmitted object change.

Comments

Popular posts from this blog

Usage of MD5 Encryption and Decryption Technology in Java Application Development

For storing mobile phone numbers of 3 billion global users, should the data type be int, string, varchar, or char? And why?