Java Serialization fundamentals

In this post, I will introduce the fundamentals of serialization in Java that you need to know.

The Serializable interface

A class that doesn’t implement the Serializable interface cannot be serialized.

In the previous post, you can see that the Car and Engine classes both implement Serializable.

The Serializable interface is a marker interface. That means it’s an interface without any method.

Your classes only need to implement that interface to be serialized.

Java Serialization principles

  • An object is serializable if
    • It implements Serializable
    • Its non-primitive fields’ types must also implement Serializable or marked as transient
  • An array is serializable if all elements are serializable
  • To ignore a field, mark it as transient
  • static fields are not serialized

Now, let’s examine these principles with some examples

Common methods

I’m going to reuse the serializeObject and deserializeObject in the previous post. Here they are for your convenience:

    private static void serializeObject(Object object, String name) throws IOException {

        try (FileOutputStream outputStream = new FileOutputStream("C:\\Users\\myn\\Downloads\\" + name);
             ObjectOutputStream objectOutputStream = new ObjectOutputStream(outputStream);) {

            objectOutputStream.writeObject(object);
        }
    }

    private static Object deserializeObject(String name) throws IOException, ClassNotFoundException {
        try (FileInputStream fileInputStream = new FileInputStream("C:\\Users\\myn\\Downloads\\" + name);
             ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);) {

            return objectInputStream.readObject();
        }
    }

Transient

Let’s consider a Phone class that has Battery as a field. Phone is serializable but Battery isn’t.

The Battery class

class Battery {
   private int capacity;

    public int getCapacity() {
        return capacity;
    }

    public void setCapacity(int capacity) {
        this.capacity = capacity;
    }
}

The Phone class:

class Phone implements Serializable {
   private Battery battery;
   private String model;
   private String manufacturer;

   public Phone(Battery battery, String model, String manufacturer) {
       this.battery = battery;
       this.model = model;
       this.manufacturer = manufacturer;
   }

    @Override
    public String toString() {
        return "Phone{" +
                "battery=" + battery +
                ", model='" + model + '\'' +
                ", manufacturer='" + manufacturer + '\'' +
                '}';
    }
}

Let’s create a phone object and serialize it and store in a file using serializeObject:

    public static void main(String[] args) throws IOException {
        
        Battery battery = new Battery();
        battery.setCapacity(300);
        
        Phone phone = new Phone(battery, "iFruit 14", "Fruit");
        
        serializeObject(phone, "fruit");

    }

What is the result of this operation?

Exception in thread "main" java.io.NotSerializableException: com.datmt.java_core.serialization.Battery
	at java.base/java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1197)
	at java.base/java.io.ObjectOutputStream.defaultWriteFields(ObjectOutputStream.java:1582)
	at java.base/java.io.ObjectOutputStream.writeSerialData(ObjectOutputStream.java:1539)
	at java.base/java.io.ObjectOutputStream.writeOrdinaryObject(ObjectOutputStream.java:1448)
	at java.base/java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1191)
	at java.base/java.io.ObjectOutputStream.writeObject(ObjectOutputStream.java:354)
	at com.datmt.java_core.serialization.TransientExample.serializeObject(TransientExample.java:28)
	at com.datmt.java_core.serialization.TransientExample.main(TransientExample.java:19)

From the log, you can see that basically, it says the class Battery is not serializable.

If you mark the battery field as transient, the serialize process will work just fine.

Let’s add transient to the battery field and try to serialize and deserialize the phone object again.

    public static void main(String[] args) throws IOException, ClassNotFoundException {

        Battery battery = new Battery();
        battery.setCapacity(300);

        Phone phone = new Phone(battery, "iFruit 14", "Fruit");

        serializeObject(phone, "fruit");

        var phone2 = (Phone) deserializeObject("fruit");

        System.out.println(phone2);
    }

This is the output:

Serialize and deserialize of transient fields
Serialize and deserialize transient fields

As you can see, the battery field is null. This is expected since it wasn’t serialized in the serialization process.

Static field serialization

As mentioned above, static fields are not serialized. It is because static fields do not belong to any object instance. Instead, the fields belong to the class.

Enum serialization

The enum class implements Serializable. Thus, you don’t need to implement Serializable again for your enums.

Let’s declare some enums and add them as fields in the Phone class:

New enums and type:

enum PhoneType {
    FLIP, TOUCH, FOLD;

}

enum ScreenType {
     NOTCH(new Screen("NOTCH")),
     DOT(new Screen("DOT"));

     private final Screen type;
     private ScreenType(Screen type) {
         this.type = type;
     }

    public Screen getType() {
        return type;
    }
}

class Screen {
    private String type;
    public Screen(String type) {
        this.type = type;
    }
}

The Phone class

class Phone implements Serializable {

    private transient Battery battery;
    private String model;
    private String manufacturer;

    private PhoneType engineType;

    private ScreenType screenType;
    public Phone(Battery battery, String model, String manufacturer) {
        this.battery = battery;
        this.model = model;
        this.manufacturer = manufacturer;
    }

    public PhoneType getPhoneType() {
        return engineType;
    }

    public void setPhoneType(PhoneType engineType) {
        this.engineType = engineType;
    }

    public ScreenType getScreenType() {
        return screenType;
    }

    public void setScreenType(ScreenType screenType) {
        this.screenType = screenType;
    }

    @Override
    public String toString() {
        return "Phone{" +
                "battery=" + battery +
                ", model='" + model + '\'' +
                ", manufacturer='" + manufacturer + '\'' +
                '}';
    }
}

As you can see, PhoneType is a simple enum and ScreenType is a bit more complex with a constructor.

Let’s create some phones and see how enum serialization/deserialization works:

    public static void main(String[] args) throws IOException, ClassNotFoundException, NoSuchFieldException {

        Battery battery = new Battery();

        Phone phone14 = new Phone(battery, "iFruit 14", "Fruit");
        Phone phone13 = new Phone(battery, "iFruit 13", "Fruit");

        phone14.setPhoneType(PhoneType.FLIP);
        phone14.setScreenType(ScreenType.DOT);

        phone13.setScreenType(ScreenType.DOT);

        System.out.println(phone13.getScreenType() == phone14.getScreenType());
        serializeObject(phone14, "phone14");
        serializeObject(phone13, "phone13");

        var phone14_restored = (Phone) deserializeObject("phone14");
        var phone13_restored = (Phone) deserializeObject("phone13");

        System.out.println(phone14_restored.getPhoneType());
        System.out.println(phone14_restored.getScreenType() == phone13_restored.getScreenType());
    }

Let’s pay attention to lines 13 and 21. What do you think the results would be?

You may think line 13 produces true and line 21 produces false.

This is the result:

Enum fields serialization
Enum fields serialization

As you can see, after serialization and deserialization, enum fields comparison works as expected.

All enums refer to the same object
All enums refer to the same object

As you can see, the ScreenType object of every phone (before and after deserialization) points to the same object.

Serialization with changes

Your programs evolve, and so do your classes. Consider a ComputerMouse class that has the following fields and methods:

class ComputerMouse implements Serializable {
    private String manufacturer; 
    public ComputerMouse(String manufacturer) {
        this.manufacturer = manufacturer;
    }

    public String getManufacturer() {
        return manufacturer;
    }
}

Let’s create an instance of this class and save it in a file called old_mouse:

    public static void main(String[] args) throws IOException, ClassNotFoundException, NoSuchFieldException {
        ComputerMouse mouse = new ComputerMouse("Genius");

        serializeObject(mouse, "old_mouse");
    }

Let’s say the class evolves and it now has some new fields and methods:

class ComputerMouse implements Serializable {
    private String manufacturer;
    private String model;
    public ComputerMouse(String manufacturer, String model) {
        this.manufacturer = manufacturer;
        this.model = model;
    }

    public String getModel() {
        return model;
    }

    public String getManufacturer() {
        return manufacturer;
    }
}

What will happen if I deserialize the “old_mouse” and cast to this new class?

    public static void main(String[] args) throws IOException, ClassNotFoundException, NoSuchFieldException {

        var mouseRestored = (ComputerMouse) deserializeObject("old_mouse");
    }

The deserialization fails and you will see an error message similar to this:

Exception in thread "main" java.io.InvalidClassException: com.datmt.java_core.serialization.ComputerMouse; local class incompatible: stream classdesc serialVersionUID = -1599228600573436250, local class serialVersionUID = 8005313661126509844
	at java.base/java.io.ObjectStreamClass.initNonProxy(ObjectStreamClass.java:728)
	at java.base/java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:2062)
	at java.base/java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1909)
	at java.base/java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2235)
	at java.base/java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1744)
	at java.base/java.io.ObjectInputStream.readObject(ObjectInputStream.java:514)
	at java.base/java.io.ObjectInputStream.readObject(ObjectInputStream.java:472)
	at com.datmt.java_core.serialization.TransientExample.deserializeObject(TransientExample.java:31)
	at com.datmt.java_core.serialization.TransientExample.main(TransientExample.java:15)

Process finished with exit code 1

It basically means the serialized object was from a class with a different serial number.

If you want to make this work, you must get the fingerprint of the old class and set it in the new class or, if you just get started, just set the static, final long variable named serialVersionUID to the class.

Let’s switch the class to the old one (without the new fields) then create an instance and serialize it again:

class ComputerMouse implements Serializable {

    public static final long serialVersionUID = 10000L;
    private String manufacturer;
    public ComputerMouse(String manufacturer) {
        this.manufacturer = manufacturer;
    }


    public String getManufacturer() {
        return manufacturer;
    }
}

public static void main(String[] args) throws IOException, ClassNotFoundException, NoSuchFieldException {
  ComputerMouse mouse = new ComputerMouse("Genius");
  serializeObject(mouse, "old_mouse");
}

Now, let’s add new fields and methods to the ComputerMouse class and deserialize “old_mouse”:

class ComputerMouse implements Serializable {

    public static final long serialVersionUID = 10000L;
    private String manufacturer;
    private String model;
    public ComputerMouse(String manufacturer, String model) {
        this.manufacturer = manufacturer;
        this.model = model;
    }

    public String getModel() {
        return model;
    }

    public String getManufacturer() {
        return manufacturer;
    }
}

public static void main(String[] args) throws IOException, ClassNotFoundException, NoSuchFieldException {
  var mouseRestored = deserializeObject("old_mouse");

  assert mouseRestored != null;

}

The code ran successfully without any exception.

If I turn on the debug mode in my IDE (intellij), I can see the values of the fields:

Deserialization with changes
Deserialization with changes

Conclusion

In this post, I’ve demonstrated the fundamentals of Serialization in java. You’ve learned how serialization works, how to deal with non-serializable fields, and how to cope with class changes.

In the next post, you will learn how to extend/customize the serialization operation using method overriding and Externalizable


Leave a Reply

Your email address will not be published.