Sunday, June 14, 2015

Inheritance of Serializable Class

Does my subclass inherit serialVersionUID of its serializable parent class?

The signature of serialVersionUID (SUID) is as below:

<ANY-ACCESS-MODIFIER> static final long serialVersionUID = <value>;

We are free to use any access modifier for SUID. Let's say, if we use protected access modifier, will it be inherited to the subclass? Check out the following code example. Guess what is the SUID value for Employee class?

class Person implements Serializable {
    protected final static long serialVersionUID = 1L;
}

class Employee extends Person {
}

By using serialver tool, we can inspect the SUID of Employee class.


"Employee" does not inherit the 1L SUID from its parent class. It is associated with a default SUID calculated by Java Serialization runtime. Although we are allowed to put any access modifier for SUID, but strongly recommended to only use the private access modifier. Mainly because SUID will not be inherited by subclass and it is useless for subclass. Moreover, it is declared as final, once it been initiated, its value can't be changed. Therefore, no point to declare it public or default as well. Read The Effect of serialVersionUID to know more about serialVersionUID.

Can I have a serializable subclass that extends non-serializable parent class?

We could have a serializable subclass that extends to a non-serializable parent class. Refer to the code example below:

class SerializationInheritanceExample {
    private static final String EMPLOYEE = "employee.ser";
    public static void main(String[] args) throws FileNotFoundException,
            IOException, ClassNotFoundException {
        
        Employee employee = new Employee();
        employee.name = "HauChee";
        employee.salary = 10000;
        
        try (ObjectOutputStream oos
                = new ObjectOutputStream(new FileOutputStream(EMPLOYEE))) {
            oos.writeObject(employee);
            oos.flush();
        }
        
        try (ObjectInputStream ois
                = new ObjectInputStream(new FileInputStream(EMPLOYEE))) {
            employee = (Employee) ois.readObject();
            // print Name: null, Salary: 10000
            System.out.printf("Name: %s, Salary: %.0f \n", 
                    employee.name, employee.salary);
        }
    }
}

class Person {
    String name;
}

class Employee extends Person implements Serializable {
    private static final long serialVersionUID = 1L;
    double salary;
}

Although "Employee" is an instance of "Person", but only "Employee's salary" field is serializable, not the name field which inherited from "Person". It is the serializable subclass's responsibility to saves and restores the non-serializable parent class's fields. This can be done by implementing special methods in Employee class. Java Serialization runtime will execute these special methods accordingly once it detected they are defined in the serializable class.

class Employee extends Person implements Serializable {
    
    private static final long serialVersionUID = 1L;
    
    double salary;
    
    // Special method to save fields into stream
    private void writeObject(ObjectOutputStream out) throws IOException {
        out.defaultWriteObject();
        out.writeObject(name); // save non-serializable name field
    }
    
    // Special method to restore fields from stream
    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
        in.defaultReadObject();
        name = (String) in.readObject(); // restore non-serialization name field
    }
}

Run the SerializationInheritanceExample again, and this time it will prints Name: HauChee, Salary: 10000. Everything working just fine. But actually to make this working, there is a prerequisite condition. The non-serializable parent class MUST have an accessible no-arg constructor to initializes the class's state. In the code example above, we do not specify any constructor for Person class. So it will by default having a public no-arg constructor, which meet the prerequisite condition. Let's make some changes to our classes.

class Person {
    String name;
    Person(String name) {
        this.name = name;
    }
}

class Employee extends Person implements Serializable {
    private static final long serialVersionUID = 1L;
    double salary;
    Employee() {
        super(null);
    }
    Employee(String name) {
        this.name = name;
    }
}

With the new changes above, during deserialization, we will hit java.io.InvalidClassException which complaining no valid constructor. This is because if we specify a constructor with argument(s) for "Person", Java compiler won't creates a public no-arg constructor for "Person" class. This break the prerequisite condition and fail the deserialization process.

When and how to use readObjectNoData method?

A sender serializes an object based on a serializable class which does not extends to a parent. The serialized object then sent to the receiver. The receiver deserializes this object based on the same SUID and same class, but this class now extends to a parent class. This causes an impact on deserialization result. readDataNoObject() is a special method for us to handling this kind of corner case. Below is the Employee class on the sender side.

class Employee implements Serializable {
    private static final long serialVersionUID = 1L;
    private double salary;
    public Employee(double salary) {
        this.salary = salary;
    }
}

By using the class definition above, sender created an "Employee" object with salary value, serialized and persisted this "Employee" object into a employee.ser file. This employee.ser file then pass to the receiver. However, same class on the receiver side has been evolved as below. Every instance of "Person" object on receiver side must associated with an id. "Employee" is an instance of "Person", therefore, the same rule applied to "Employee". Below is the Employee class on the receiver side.

class Employee extends Person implements Serializable { //Employee has a parent now
    private static final long serialVersionUID = 1L;
    private double salary;
    public Employee(String id, double salary) { //require id now
        super(id);
        this.salary = salary;
    }
}

class Person implements Serializable {
    private static final long serialVersionUID = 1L;
    private String id;
    public Person(String id) {
        if (id == null || id.length() == 0) { //enforcement, id is mandatory
            throw new IllegalArgumentException("Must provide id!");
        }
        this.id = id;
    }
}

If receiver simply deserializes employee.ser< as below,

class DeserializeEmployeeApp {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        try (ObjectInputStream ois
                = new ObjectInputStream(new FileInputStream("employee.ser"))) {
            Employee employee = (Employee) ois.readObject();
            System.out.printf("id: %s, salary: %.0f \n", // print id: null, salary: 1000
                    employee.id, employee.salary);
        }
    }
}

The receiver will get id: null, salary: 1000. This has violated the mandatory id rule. In this case, the best candidate for handling this situation is the parent class because it knows well of the rule it introduced, and how to deal with deserialized object properly. Parent class can do this by implementing readObjectNoData() special method.

If Person class choose to assign a default id for deserialized employee object, then it can add the following method.

private void readObjectNoData() {
    this.id = "TEMP_ID";
}

You may wondering why not just implement a no-arg constructor in Person and assign a default value to id field? This does not help because Person is also a serializable class. Java Serialization runtime won't call its no-arg constructor to constitutes "Person" fields. Instead, the "Person" fields are supposed to be re-constituted straight away from stream data. The is why readObjectNoData() method come in and play the role of constructor during deserialization.

If Person class decided that, no exceptional case for any "Person" instance without id, even a deserialized object, then it can throws java.io.InvalidObjectException in the readObjectNoData() method. In this case, the mandatory id invariant could be maintained. Read Security in Java Serialization to learn more.


private void readObjectNoData() {
    throw new InvalidObjectException("Id must not be null."); 
}

Reference:
http://docs.oracle.com/javase/7/docs/api/java/io/Serializable.html

No comments: