java Foundation series 21 (serialization and deserialization)

1, Introduction to serialization

In the project, there are many situations that need to serialize and deserialize the instance object, so that the state of the object can be saved persistently, and even object transfer and remote call can be carried out among various components. Serialization mechanism is an indispensable and common mechanism in projects.

The simplest way for a class to have the functions of serialization and deserialization is to implement the java.io.Serializable interface, which is a marker Interface, that is, it has no field and method definitions.

After we define a class that implements the Serializable interface, we will generally manually define a private static final long serialVersionUID field inside the class to save the serial version number of the current class. The purpose of this is to uniquely identify the class. After the object is persisted, this field will be saved to the persistence file. When we make some changes to this class, the new changes can find the persisted content according to the version number to ensure that the changes from the class can be accurately reflected in the persisted content. The original persistent content cannot be found because the version number is not defined.

Of course, if we serialize and deserialize this class without implementing the Serializable interface, we will throw a java.io.NotSerializableException.

Examples are as follows:

public class Student implements Serializable {
    private static final long serialVersionUID = -3111843137944176097L;
    private String name;
    private int age;
    private String sex;
    private String address;
    private String phone;
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public int getAge() {
        return age;
    }
    public void setAge(int age) {
        this.age = age;
    }
    public String getSex() {
        return sex;
    }
    public void setSex(String sex) {
        this.sex = sex;
    }
    public String getAddress() {
        return address;
    }
    public void setAddress(String address) {
        this.address = address;
    }
    public String getPhone() {
        return phone;
    }
    public void setPhone(String phone) {
        this.phone = phone;
    }
}

2, Use of serialization

To realize serialization, we only need to implement the Serializable interface, but this only allows the class object to have the function of serialization and deserialization. It will not automatically realize serialization and deserialization. We need to write code for serialization and deserialization.

This requires the writeObject() method and readObject() method of ObjectOutputStream class. These two methods correspond to writing objects to the stream (serialization) and reading objects from the stream (deserialization).

What is the serialization of objects in Java? The answer is the state of the object, more specifically the fields in the object and their values, because these values just describe the state of the object.

In the following example, we can persist an instance of the Student class to the local file "D:/student.out" and read the memory from the local file. This is achieved with the help of FileOutputStream and FileInputStream:

public class SerilizeTest {
    public static void main(String[] args) {
        serilize();
        Student s = (Student) deserilize();
        System.out.println("full name:" + s.getName()+"\n Age:"+ s.getAge()+"\n Gender:"+s.getSex()+"\n Address:"+s.getAddress()+"\n mobile phone:"+s.getPhone());
    }
    public static Object deserilize(){
        Student s = new Student();
        InputStream is = null;
        ObjectInputStream ois = null;
        File f = new File("D:/student.out");
        try {
            is = new FileInputStream(f);
            ois = new ObjectInputStream(is);
            s = (Student)ois.readObject();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }finally{
            if(ois != null){
                try {
                    ois.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if(is != null){
                try {
                    is.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        return s;
    }
    
    public static void serilize() {
        Student s = new Student();
        s.setName("Zhang San");
        s.setAge(32);
        s.setSex("man");
        s.setAddress("Beijing");
        s.setPhone("12345678910");
//      s.setPassword("123456");
        OutputStream os = null;
        ObjectOutputStream oos = null;
        File f = new File("D:/student.out");
        try {
            os = new FileOutputStream(f);
            oos = new ObjectOutputStream(os);
            oos.writeObject(s);
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }finally{
            if(oos != null)
                try {
                    oos.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            if(os != null)
                try {
                    os.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
        }
    }
}

Through the above code, you can realize simple object serialization and deserialization.

Execution results:

Name: Zhang San
 Age: 32
 Gender: man
 Address: Beijing
 Mobile: 12345678910

The call stack of writeObject is listed here:

writeObject->writeObject0->writeOrdinaryObject->writeSerialData->defaultWriteFields->writeObject0->...

At the end of the call stack, the writeObject0 method is returned. This is to recursively traverse all the type fields of the common Serializable interface in the fields of the target class and write them all to the stream. Finally, all writes will be terminated in the writeObject0 method. This method will call the write method of the response according to the type of the field for stream writing.

Java serializes the fields of objects, but these fields are not necessarily simple strings or integers. They may also be very complex types. A class type that implements the Serializable interface. At this time, when serializing, we need to recursively serialize the internal second-level objects. This nesting can have countless layers, But there will always be an end.

3, Custom serialization function

The above contents are simple and simple. What we really need to pay attention to here is that the content about custom serialization strategy is the most important and complex content in the serialization mechanism.

3.1 use of transient keyword

As mentioned above, Java serializes the object's non static fields and their values. The transient keyword is used in the fields of the target class that implements the Serializable interface. All fields modified by this keyword will be serialized and filtered out, that is, they will not be serialized.

Add the transient keyword before the phone field in the Student class in the above example:

public class Student implements Serializable {
    //...
    private transient String phone;
    //...
}

The execution result becomes:

Name: Zhang San
 Age: 32
 Gender: man
 Address: Beijing
 mobile phone: null

It can be seen that because the transient keyword is added to the phone field, its value is not serialized during serialization. After deserialization, its value will be null.

3.2 use of writeobject method

writeObject() is a method defined in ObjectOutputStream. Using this method, you can write the target object to the stream to serialize the object. However, Java provides us with the function of customizing the writeObject() method. When we customize the writeObject() method in the target class, we will first call our customized method, and then continue to execute the original method steps (using the defaultWriteObject method). This function allows us to perform some additional operations on the fields of the object before object serialization. The most common is to take effective encryption measures for some fields that need to be kept confidential (such as password fields) to ensure the security of persistent data.

Here, I add the password field and the corresponding set and get methods to the Student class.

public class Student implements Serializable {
    //...
    private String password;
    public String getPassword() {
        return password;
    }
    public void setPassword(String password) {
        this.password = password;
    }
}

Then define the writeObject() method in the Student class:

public class Student implements Serializable {
    //...
    private void writeObject(ObjectOutputStream oos) throws IOException{
        password = Integer.valueOf(Integer.valueOf(password).intValue() << 2).toString();
        oos.defaultWriteObject();
    }
}

Here I simply encrypt the value of the password field in the way of two bits left, and then call the defaultWriteObject() method in ObjectOutputStream to return to the original serialization execution step. The specific call stack is as follows:

writeObject->writeObject0->writeOrdinaryObject->writeSerialData->invokeWriteObject->invoke(Call custom writeObject)->defaultWriteObject->defaultWriteFields->writeObject0->...

After adding the writeObject method to the target class, we can see from the above call stack that the call sequence will turn at writeSerialData. Execute the invokeWriteObject method, call the writeObject method in the target class, and then go back to the original step through the defaultWriteObject method, indicating that the customized writeObject method operation will take precedence.
After this setting, after serialization, the encrypted password value will be saved in the file. We will test it in combination with the readObject method of the next content.

3.3 use of readObject method

This method corresponds to the writeObject method. It is used to read the serialized content and is used in the deserialization process. Similar to the customization of writeObject method, we customize readObject method:

public class Student implements Serializable {
    //...
    private void readObject(ObjectInputStream ois)throws IOException, ClassNotFoundException{
        ois.defaultReadObject();
        if(password != null)
            password = Integer.valueOf(Integer.valueOf(password).intValue() >> 2).toString();
    }
}

To add a password field to the test program:

public class SerilizeTest {
    public static void main(String[] args) {
        serilize();
        Student s = (Student) deserilize();
        System.out.println("full name:" + s.getName()+"\n Age:"+ s.getAge()+"\n Gender:"+s.getSex()+"\n Address:"+s.getAddress()+"\n mobile phone:"+s.getPhone()+"\n password:"+s.getPassword());
        
    }
    //...
    public static void serilize() {
        Student s = new Student();
        s.setName("Zhang San");
        s.setAge(32);
        s.setSex("man");
        s.setAddress("Beijing");
        s.setPhone("12345678910");
        s.setPassword("123456");
        OutputStream os = null;
        ObjectOutputStream oos = null;
        File f = new File("D:/student.out");
        //...
    }
}

The result of executing the procedure is:

Name: Zhang San
 Age: 32
 Gender: man
 Address: Beijing
 mobile phone: null
 Password: 123456

The password here has been encrypted during serialization and deserialization. Since the results are consistent, the change cannot be seen. The simple way is to change the decryption algorithm:

public class Student implements Serializable {
    //...
    private void readObject(ObjectInputStream ois)throws IOException, ClassNotFoundException{
        ois.defaultReadObject();
        if(password != null)
            password = Integer.valueOf(Integer.valueOf(password).intValue() >> 3).toString();
    }
}

Here, the decryption algorithm is changed to shift the target value to the right by three bits, which will cause the last obtained password value to be different from the original "123456". The results are as follows:

Name: Zhang San
 Age: 32
 Gender: man
 Address: Beijing
 mobile phone: null
 Password: 61728

3.4 use of writereplace method

Java serialization is not dead, but very flexible. We can even change the target type during serialization, which requires the writeReplace method to operate.

We customize the writeReplace method in the target class. This method is used to return an Object type. This Object is the type you changed. During serialization, we will judge whether there is a writeObject method in the target class. If there is such a method, we will call it and use the type Object returned by this method as the new target Object of serialization.

Now let's customize the writeReplace method in the Student class:

public class Student implements Serializable {
    //...
    private Object writeReplace() throws ObjectStreamException{
        StringBuffer sb = new StringBuffer();
        String s = sb.append(name).append(",").append(age).append(",").append(sex).append(",").append(address).append(",").append(phone).append(",").append(password).toString();
        return s;
    }
}

The data in the target class is integrated into a string through the custom writeReplace method, and the string is serialized as a new target object.

An error will be reported after execution:

Exception in thread "main" java.lang.ClassCastException: java.lang.String cannot be cast to xuliehua.Student
    at xuliehua.SerilizeTest.deSerilize(SerilizeTest.java:32)
    at xuliehua.SerilizeTest.main(SerilizeTest.java:20)

Prompt: during deserialization, the string type cannot be forcibly converted to Student type, which indicates that the serialized content saved to the file is string type, that is, our custom writeReplace method works.

Now let's make some modifications to the deserialization method to accurately obtain the serialized content.

public class SerilizeTest {
    public static void main(String[] args) {
        serilize();
        Student s = (Student) deserilize();
//        System.out.println("Name:" + s.getName()+"\n age:" + s.getAge()+"\n gender:" + s.getSex()+"\n address:" + s.getAddress()+"\n mobile phone:" + s.getPhone()+"\n password:" + s.getPassword());
        System.out.println(s);
    }
    //...
    public static String deSerilize(){
//      Student s = new Student();
        String s = "";
        InputStream is = null;
        ObjectInputStream ois = null;
        File f = new File("D:/student.out");
        try {
            is = new FileInputStream(f);
            ois = new ObjectInputStream(is);
//            s = (Student)ois.readObject();
            s = (String)ois.readObject();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }finally{
            if(ois != null){
                try {
                    ois.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if(is != null){
                try {
                    is.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        return s;
    }
}

Let's do it again:

Zhang San,32,man,Beijing,12345678910,123456

Get serialized content exactly.

It should be noted here that when we use this method to change the target object type, the filtering function of the field identified as transient in the original type will fail, because of the transfer of the serialized target. Naturally, the transient set in the original type field will not play any role in the new type, just like the phone field.

3.5 use of readresolve method

Corresponding to the writeReplace method, we can also change the target type during deserialization. This requires the use of the readResolve method. The use method is to customize the readResolve method in the target class. The return value of this method is the Object object, that is, the converted new type Object.

Here, we modify the code based on 3.3. First, we customize the readResolve method in the Student class:

public class Student implements Serializable {
    //...
//    private Object writeReplace() throws ObjectStreamException{
//        StringBuffer sb = new StringBuffer();
//        String s = sb.append(name).append(",").append(age).append(",").append(sex).append(",").append(address).append(",").append(phone).append(",").append(password).toString();
//        return s;
//    }
    private Object readResolve()throws ObjectStreamException{
        Map<String,Object> map = new HashMap<String,Object>();        
        map.put("name", name);
        map.put("age", age);
        map.put("sex", sex);
        map.put("address", address);
        map.put("phone", phone);
        map.put("password", password);
        return map;
    }
}

In this method, we save the obtained data to a Map collection and return the collection.

An error will be reported if the program is executed directly:

 Exception in thread "main" java.lang.ClassCastException: java.util.HashMap cannot be cast to xuliehua.Student
     at xuliehua.SerilizeTest.deSerilize(SerilizeTest.java:32)
     at xuliehua.SerilizeTest.main(SerilizeTest.java:20)

The error report indicates that the readResolve method we set has been executed. Because the type cannot be converted, an error is reported. We make the following modifications:

public class SerilizeTest {
    public static void main(String[] args) {
        serilize();
//        Student s = (Student) deserilize();
//        System.out.println("Name:" + s.getName()+"\n age:" + s.getAge()+"\n gender:" + s.getSex()+"\n address:" + s.getAddress()+"\n mobile phone:" + s.getPhone()+"\n password:" + s.getPassword());
        Map<String,Object> map = deSerilize();
        System.out.println(map);
    }
    //...
    @SuppressWarnings("unchecked")
    public static Map<String,Object> deSerilize(){
        Map<String,Object> map = new HashMap<String,Object>();
//        Student s = new Student();
        InputStream is = null;
        ObjectInputStream ois = null;
        File f = new File("D:/student.out");
        try {
            is = new FileInputStream(f);
            ois = new ObjectInputStream(is);
//            s = (Student)ois.readObject();
            map = (Map<String,Object>)ois.readObject();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }finally{
            if(ois != null){
                try {
                    ois.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if(is != null){
                try {
                    is.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        return map;
    }
}

Execution results:

{phone=null, sex=man, address=Beijing, age=32, name=Zhang San, password=61728}

It can be seen that we can accurately obtain the data, and it is based on the changed type.

Note: the writeObject method and the readObject method can exist at the same time, but generally, the writeReplace method and the readResolve method are not used at the same time. Because both types are converted based on the original type, if they exist at the same time, type conversion cannot be performed between the two new types (unless of course there is an inheritance relationship between the two types), and the function cannot be realized.

Keywords: Java Spring

Added by TouranMan on Wed, 17 Nov 2021 08:13:48 +0200