Friday, October 29, 2021

Java Tutorial: Serialization and Deserialization

Chapters

Serialization and Deserialization

Serialization is a process of writing primitive types and state of object types to an output stream. We use ObjectOutputStream to process serialization. Deserialization is a process of reading serialized objects and primitive types from an input stream. We use ObjectInputStream to process deserialization.

Note: Static elements like static class and its members are not serializable because they belong to the class itself not to the class instance. Serialization only serializes class instance and its members.

Serialization Using ObjectOutputStream

This example demonstrates serialization using ObjectOutputStream. To serialize an object of a class, the class must implement Serializable interface.

Note: Objects that come out from inner, local and anonymous classes are discouraged by java to be serialized.
import java.io.File;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.io.FileOutputStream;
import java.io.Serializable;

public class SampleClass{

  public static void main(String[] args)
                     throws IOException{
    
    Profile profileOne = new Profile("John", 22,
                         "Johnny",
                         new Message("Hello!"),
                         1442);
    
    //when serializing an array of objects, be
    //sure that the objects in the array are
    //all serializable or else
    //serialization will fail
    Data[] datArr = new Data[2];
    
    datArr[0] = new Profile("Mark", 20,
                            "Marky",
                            new Message("Hi!"),
                            1322);
                            
    datArr[1] = new Profile("Dolly", 18,
                            "lily",
                            new Message("Hey!"),
                            40298);
    
    File file = new File("C:\\test\\test.obj");
    try(ObjectOutputStream oos =
        new ObjectOutputStream(
            new FileOutputStream(file)) ){
            
      oos.writeObject(profileOne);
      oos.writeObject(datArr);
      
      //flush ObjectOutputStream
      //so, the data can be written
      //to the underlying output stream
      oos.flush();
    }
    System.out.println("Done!");
  }
}

//a class must implement Serializable interface
//to make its objects serializable
//if the serializable class has subclasses,
//those subclasses are also serializable
abstract class Data implements Serializable{
  
  //if a serializable class has class
  //references, those references must
  //be serializable. Otherwise, java
  //will throw NotSerializableException
  //when you try to serialize a
  //serializable class with
  //class reference that is not
  //serializable
  private String name;
  private int age;
  
  Data(String name, int age){
    this.name = name;
    this.age = age;
  }
  
  public String getName(){
    return name;
  }
  
  public int getAge(){
    return age;
  }
}

class Profile extends Data{

  private String nickName;
  
  //we can add a transient modifier
  //to a class reference that are
  //not serializable to avoid
  //NotSerializableException if
  //the unserializable class reference
  //is in a serializable class
  private transient Message message;
  
  //by default, static elements
  //are not serializable
  //because static elements
  //are not part of an object
  //Though, they don't trigger
  //NotSerializableException
  //adding transient in a static
  //field is redundant
  private static int sessionNo;
  
  Profile(String name, int age, String nickName,
          Message message, int session){
    super(name, age);
    this.nickName = nickName;
    this.message = message;
    sessionNo = session;
  }
  
  public static int getSessionNo(){
    return sessionNo;
  }
  
  public String getNickName(){
    return nickName;
  }
  
  public String getMessage(){
    String str = null;
    
    if(message != null)
      str = message.getMessage();
    
    return str;
  }
}

class Message{
  
  private String msg;
  
  Message(String msg){
    this.msg = msg;
  }
  
  public String getMessage(){
    return msg;
  }
}
transient Keyword

Variables that have transient keyword won't be included in the serialization process. In the example above, we use the transient keyword in an object reference. Though, transient keyword can be used for primitive and object type variables. The transient keyword is used to avoid NotSerializableException. This exception occurs if an unserializable class has a reference in a serializable class.

In they example above, Message class is not serializable that's why I put transient keyword in the message variable in Profile class which is a serializable class. Try removing the transient keyword and you will see a NotSerializableException.

Deserialization Using ObjectInputStream

This example demonstrates serialization using ObjectOutputStream.
Note: the example below requires test.obj that can be created by executing the example code in the Serialization Using ObjectOutputStream topic.

Warning: Deserialization of untrusted data is inherently dangerous and should be avoided. Untrusted data should be carefully validated according to the "Serialization and Deserialization" section of the Secure Coding Guidelines for Java SE. Serialization Filtering describes best practices for defensive use of serial filters.
import java.io.File;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.FileInputStream;
import java.io.Serializable;

public class SampleClass{

  public static void main(String[] args)
         throws IOException, ClassNotFoundException{
    
    File file = new File("C:\\test\\test.obj");
    try(ObjectInputStream ois =
        new ObjectInputStream(
            new FileInputStream(file)) ){
      
      //Read object types
      //read the source data according to
      //the order of how the source data
      //have been written
      Profile p1 = (Profile)ois.readObject();
      Data[] arr = (Data[])ois.readObject();
      System.out.println("Deserialization Complete!");
      System.out.println();
      
      System.out.println("Profile #1");
      System.out.println("name: " + p1.getName());
      System.out.println("age: " + p1.getAge());
      System.out.println("nickname: " + p1.getNickName());
      System.out.println("message: " + p1.getMessage());
      System.out.println("session #: " + p1.getSessionNo());
      System.out.println();
      
      System.out.println("Profile #2");
      Profile p2 = (Profile)arr[0];
      System.out.println("name: " + p2.getName());
      System.out.println("age: " + p2.getAge());
      System.out.println("nickname: " + p2.getNickName());
      System.out.println("message: " + p2.getMessage());
      System.out.println("session #: " + p2.getSessionNo());
      
    }
  }
}

abstract class Data implements Serializable{
  
  private String name;
  private int age;
  
  Data(String name, int age){
    this.name = name;
    this.age = age;
  }
  
  public String getName(){
    return name;
  }
  
  public int getAge(){
    return age;
  }
}

class Profile extends Data{

  private String nickName;
  
  private transient Message message;
  
  private static int sessionNo;
  
  Profile(String name, int age, String nickName,
          Message message, int session){
    super(name, age);
    this.nickName = nickName;
    this.message = message;
    sessionNo = session;
  }
  
  public static int getSessionNo(){
    return sessionNo;
  }
  
  public String getNickName(){
    return nickName;
  }
  
  public String getMessage(){
    String str = null;
    
    if(message != null)
      str = message.getMessage();
    
    return str;
  }
}

class Message{
  
  private String msg;
  
  Message(String msg){
    this.msg = msg;
  }
  
  public String getMessage(){
    return msg;
  }
}

Result
Deserialization Complete!

Profile #1
name: John
age: 22
nickname: Johnny
message: null
session #: 0

Profile #2
name: Mark
age: 20
nickname: Marky
message: null
session #: 0
In the result above, message is null and session # is 0 because message variable is transient and sessionNo variable is static.
SerialVersionUID

SerialVersionUID is used to verify serializable class between the receiver and the sender. The SerialVersionUID of a serializable class in the sender's program must be the same as the SerialVersionUID of the serializable class in the receiver's program.

Otherwise, InvalidClassException will be thrown during deserialization. We can manually declare a serializable class's SerialVersionUID. If SerialVersionUID is not declared, java will automatically generate a SerialVersionUID for that class. This is how we declare a SerialVersionUID.
private static final long SerialVersionUID = 1L;
You can change the value on the right side of the equal operator but you shouldn't change the declaration expression on the left side. It's strongly recommended to manually declare SerialVersionUID in serializable classes, except for serializable enum classes, to avoid unwanted compatibility issues. Let's try adding SerialVersionUID in a serializable class.
abstract class Data implements Serializable{
  
  //explicit declaration of SerialVersionUID
  private static final long SerialVersionUID = 100L;
  
  private String name;
  private int age;
  
  Data(String name, int age){
    this.name = name;
    this.age = age;
  }
  
  public String getName(){
    return name;
  }
  
  public int getAge(){
    return age;
  }
}
Custom Way of Reading and Writing Serializable Objects

Java has a default way of reading/writing serializable objects. However, we can override that default way by defining a readObject() and a writeObject() methods in a serializable class. In this way, java will execute the readObject() and writeObject() that we defined in the serializable class everytime we serialize/deserialize objects of that serializable class.

This customization is particularly used when we want to serialize some serializable fields from unserializable object reference.
import java.io.File;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.Serializable;

public class SampleClass{

  public static void main(String[] args)
                     throws IOException,
                            ClassNotFoundException{
    
    File file = new File("C:\\test\\testObj.obj");
    try(ObjectOutputStream oos =
        new ObjectOutputStream(
            new FileOutputStream(file));
        ObjectInputStream ois =
        new ObjectInputStream(
            new FileInputStream(file))){
            
        Data input = new Data("John", 22,
                    new Profile(1225,"Johnny",
                                "Hello!"));
        oos.writeObject(input);
        Data output = (Data)ois.readObject();
    }
    
  }
}

class Data implements Serializable{
  private static long SerialVersionUID = 1L;
  
  private String name;
  private int age;
  private transient Profile profile;
  
  Data(String name, int age,
       Profile profile){
    this.name = name;
    this.age = age;
    this.profile = profile;
  }
  
  //define custom writeObject() method
  private void writeObject(ObjectOutputStream oos)
                           throws IOException{
    
    System.out.println("Writing Objects...");
    //this method serializes serializable and
    //non-transient fields
    oos.defaultWriteObject();
    
    //In this way, we are able to serialize
    //the value of the message variable in 
    //profile object.
    if(profile != null)
      oos.writeObject(profile.getMessage());
  }
  
  //define custom readObject() method
  private void readObject(ObjectInputStream ois)
          throws IOException, ClassNotFoundException{
    
    System.out.println("Reading Objects...");
    //this method reads serialized
    //objects of the Data object
    ois.defaultReadObject();
    
    //read the serialized message
    //that is not a member of Data object
    System.out.println(name+" Previous Message: " +
                       ois.readObject());
  }
  
  String getName(){
    return name;
  }
  
  int getAge(){
    return age;
  }
  
  void setProfile(Profile profile){
    this.profile = profile;
  }
}

class Profile{

  private int sessionNo;
  private String nickName;
  private String message;
  
  Profile(int sessionNo, String nickName,
          String message){
          
    this.sessionNo = sessionNo;
    this.nickName = nickName;
    this.message = message;
  }
  
  int getSessionNo(){
    return sessionNo;
  }
  
  public String getNickName(){
    return nickName;
  }
  
  public String getMessage(){
    return message;
  }
  
  void setMessage(String message){
    this.message = message;
  }
}

Result
Writing Objects...
Reading Objects...
John Previous Message: Hello!
Externalizable Interface

Externalizable is just like Serializable. Although, Externalizable gives full control of serialization/deserialization process to the class itself.
import java.io.File;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectInput;
import java.io.ObjectOutputStream;
import java.io.ObjectOutput;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.Externalizable;

public class SampleClass{

  public static void main(String[] args)
                     throws IOException,
                            ClassNotFoundException{
                            
    File file = new File("C:\\test\\testObj.obj");
    try(ObjectOutputStream oos =
        new ObjectOutputStream(
            new FileOutputStream(file));
        ObjectInputStream ois =
        new ObjectInputStream(
            new FileInputStream(file))){
      
      Data input = new Data(22, "John", 3.5f);
      input.writeExternal(oos);
      
      //flush ObjectOutputStream so the
      //data can be written to the undelying
      //output stream
      oos.flush();
      
      //when deserializing, we need to create
      //an object first before calling
      //readExternal()
      Data output = new Data();
      output.readExternal(ois);
      
      System.out.println("output");
      System.out.println("name: " + output.name);
      System.out.println("age: " + output.age);
      System.out.println("score: " + output.score);
    }
    
  }
}

class Data implements Externalizable{
  private static long SerialVersionUID = 1L;
  
  Integer age;
  String name;
  Float score;
  
  Data(int age, String name, float score){
  
    this.age = age;
    this.name = name;
    this.score = score;
  }
  
  //Default Constructor
  Data(){}
  
  @Override
  public void writeExternal(ObjectOutput out)
                            throws IOException{
    System.out.println("Writing Serializable Fields");
    out.writeInt(age);
    out.writeUTF(name);
    out.writeFloat(score);
  }
  
  @Override
  public void readExternal(ObjectInput in)
                           throws IOException,
                                  ClassNotFoundException{
    System.out.println("Reading Serializable Fields");
    this.age = in.readInt();
    this.name = in.readUTF();
    this.score = in.readFloat();
  }
  
}

Result
Writing Serializable Fields
Reading Serializable Fields
output
name: John
age: 22
score: 3.5
When a serializable class that extends Externalizable has subclass, the subclass needs to override writeExternal() and readExternal() methods and call writeExternal() and readExternal() of the super class. This will ensure that the serializable fields in the super class are included in the serialization/deserialization process in the subclass.
import java.io.File;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectInput;
import java.io.ObjectOutputStream;
import java.io.ObjectOutput;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.Externalizable;

public class SampleClass{

  public static void main(String[] args)
                     throws IOException,
                            ClassNotFoundException{
                            
    File file = new File("C:\\test\\testObj.obj");
    try(ObjectOutputStream oos =
        new ObjectOutputStream(
            new FileOutputStream(file));
        ObjectInputStream ois =
        new ObjectInputStream(
            new FileInputStream(file))){
      
      Profile input = new Profile(22, "John",
                     "Johnny", 2250, 55);
      input.writeExternal(oos);
      
      //flush ObjectOutputStream so the
      //data can be written to the undelying
      //output stream
      oos.flush();
      
      Profile output = new Profile(3307);
      output.readExternal(ois);
      
      System.out.println("output");
      System.out.println("name: " + output.name);
      System.out.println("age: " + output.age);
      System.out.println("nickname: " + output.nickname);
      System.out.println("score: " + output.score);
    }
    
  }
}

abstract class Data implements Externalizable{
  private static long SerialVersionUID = 1L;

  Integer age;
  String name;
  
  Data(int age, String name){
  
    this.age = age;
    this.name = name;
  }
  
  //Default Constructor
  Data(){}
  
  @Override
  public void writeExternal(ObjectOutput out)
                            throws IOException{
    out.writeInt(age);
    out.writeUTF(name);
  }
  
  @Override
  public void readExternal(ObjectInput in)
                           throws IOException,
                                  ClassNotFoundException{
    this.age = in.readInt();
    this.name = in.readUTF();
  }
  
}

class Profile extends Data{
  private static long SerialVersionUID = 1L;
  
  String nickname;
  private int sessionNo;
  int score;
  
  Profile(int age, String name, String nickname,
          int sessionNo, int score){
          
    super(age,name);
    this.nickname = nickname;
    this.sessionNo = sessionNo;
    this.score = score;
  }
  
  
  Profile(int sessionNo){
    this.sessionNo = sessionNo;
  }
  
  @Override
  public void writeExternal(ObjectOutput out)
                            throws IOException{
    super.writeExternal(out);
    out.writeUTF(nickname);
    out.writeInt(score);
  }
  
  @Override
  public void readExternal(ObjectInput in)
                           throws IOException,
                                  ClassNotFoundException{
    super.readExternal(in);
    nickname = in.readUTF();
    score = in.readInt();
  }
}

Result
output
name: John
age: 22
nickname: Johnny
score: 55
In the example above, sessionNo has been skipped. When serializing/deserializing object with Externalizable interface, We don't need to add transient modifier to the fields that we don't wanna be serialized. We just skip them.
Differences Between Externalizable and Serializable

These two interfaces are closely similar to each other. Although, these two have different usage. These are the differences between these two interfaces:
  • Externalizable requires two methods to be overriden. Serializable doesn't require any methods to be overriden.
  • Serializable is easy to implement and the JVM does most of the job in serialization/deserialization process. Externalizable is tedious to implement and the implementor is responsible for the logical process of serialization/deserialization
  • Serializable has internal serialization/deserialization processes that our project may not need whereas in Externalizable, we decide the serialization/deserialization logic. Thus, Using externalizable often yields better performance.
  • In Serializable, we add transient keyword to any fields that we don't want to be serialized whereas in Externalizable, we just skip fields that we don't want to be serialized.
  • When doing simple serialization/deserialization task, Serializable will suffice. When doing advanced serialization/deserialization task, consider using Externalizable

No comments:

Post a Comment