Do you really know about Java serialization?

Introduction

When persisting data objects, we seldom use Java serialization, but use database and other methods to realize it. But in my opinion, Java serialization is a very important content. Serialization can not only save objects to disk for persistence, but also transmit them over the network. In the normal interview, serialization is also often talked about.

When it comes to serialization, you may know that the Serializable interface can achieve serialization, but when we see the interview questions about serialization, we are often confused.

1) What is the difference between a serializable interface and an external interface?

2) When serializing, do you want some members not to serialize? How to achieve it?

3) What is serialVersionUID? What happens if the serialVersionUID is not defined?

Is it suddenly found that we still have a lot of doubts about these problems? This article will summarize some common problems of Java serialization, and test and answer them with demo.

Question 1: what is Java serialization?

Serialization is the process of changing objects to binary format that can be saved to disk or sent to other running Java virtual machines through the network, and can recover object state through deserialization. Java serialization API provides a standard mechanism for developers: through the implementation of java.io.Serializable or java.io.Externalizable interface, ObjectInputStream and ObjectOutputStream handle object serialization. To implement the java.io.Externalizable interface, Java programmers are free to choose standard serialization based on class structure or their customized binary format, which is generally considered as the best practice, because the serialized binary format becomes part of the class output API, which may destroy the encapsulation of private and package visible attributes in Java.

What's the use of serialization?

Implement java.io.Serializable.

Define user class:

class User implements Serializable {
	private String username;
    private String passwd;

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPasswd() {
        return passwd;
    }

    public void setPasswd(String passwd) {
        this.passwd = passwd;
    }
}

We serialize the object, store it in the txt file through the ObjectOutputStream, read the txt file through the ObjectInputStream, and deserialize it into the User object.

public class TestSerialize {

    public static void main(String[] args) {

        User user = new User();
        user.setUsername("hengheng");
        user.setPasswd("123456");

        System.out.println("read before Serializable: ");
        System.out.println("username: " + user.getUsername());
        System.err.println("password: " + user.getPasswd());

        try {
            ObjectOutputStream os = new ObjectOutputStream(
                    new FileOutputStream("/Users/admin/Desktop/test/user.txt"));
            os.writeObject(user); // Write User object to file
            os.flush();
            os.close();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
        try {
            ObjectInputStream is = new ObjectInputStream(new FileInputStream(
                    "/Users/admin/Desktop/test/user.txt"));
            user = (User) is.readObject(); // Read User's data from the stream
            is.close();

            System.out.println("\nread after Serializable: ");
            System.out.println("username: " + user.getUsername());
            System.err.println("password: " + user.getPasswd());

        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

The operation results are as follows:

Data before serialization: 
username: hengheng
password: 123456

Serialized data: 
username: hengheng
password: 123456

By now, we've probably learned what serialization is.

Question 2: when serializing, you want some members not to serialize. How to implement it?

Answer: declare that the member is static or transient and will not be serialized during Java serialization.

  • Static variable: add static keyword.
  • Transient variable: add the transient keyword.

Let's try to declare variables as transients first.

class User implements Serializable {
    private String username;
    private transient String passwd;

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPasswd() {
        return passwd;
    }

    public void setPasswd(String passwd) {
        this.passwd = passwd;
    }

Add the transient keyword to the password field before running. Operation result:

Data before serialization: 
username: hengheng
password: 123456

Serialized data: 
username: hengheng
password: null

The result shows that the password is not serialized, which achieves our goal.

Try adding the static keyword before the user name.

class User implements Serializable {
    private static String username;
    private transient String passwd;

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPasswd() {
        return passwd;
    }

    public void setPasswd(String passwd) {
        this.passwd = passwd;
    }

Operation result:

Data before serialization: 
username: hengheng
password: 123456

Serialized data: 
username: hengheng
password: null

We found that the result after running is not the same as expected, so the username should be null. What's the reason?

The reason is that the value of the static variable username in the class after deserialization is the value of the corresponding static variable in the current JVM, rather than the result of deserialization.

Let's prove:

public class TestSerialize {

    public static void main(String[] args) {

        User user = new User();
        user.setUsername("hengheng");
        user.setPasswd("123456");

        System.out.println("Data before serialization: ");
        System.out.println("username: " + user.getUsername());
        System.err.println("password: " + user.getPasswd());

        try {
            ObjectOutputStream os = new ObjectOutputStream(
                    new FileOutputStream("/Users/admin/Desktop/test/user.txt"));
            os.writeObject(user); // Write User object to file
            os.flush();
            os.close();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
        User.username = "Xiao Ming";
        try {
            ObjectInputStream is = new ObjectInputStream(new FileInputStream(
                    "/Users/admin/Desktop/test/user.txt"));
            user = (User) is.readObject(); // Read User's data from the stream
            is.close();

            System.out.println("\n Serialized data: ");
            System.out.println("username: " + user.getUsername());
            System.err.println("password: " + user.getPasswd());

        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

class User implements Serializable {
    public static String username;
    private transient String passwd;

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPasswd() {
        return passwd;
    }

    public void setPasswd(String passwd) {
        this.passwd = passwd;
    }
}

Change the value of the static variable username to Xiaoming before deserializing.

User.username = "Xiao Ming";

Run again:

Data before serialization: 
username: hengheng
password: 123456

Serialized data: 
username: Xiaoming
password: null

Sure enough, the username here is the value of the static variable in the JVM, not the value from deserialization.

Question 3: what's the use of serialVersionUID?

We often customize a serialVersionUID in the class:

private static final long serialVersionUID = 8294180014912103005L

What's the use of this serialVersionUID? What are the consequences if it is not set?

SerialVersionUID is a private static final long type ID. when it is printed on an object, it is usually the hash code of the object. The serialVersionUID can be defined or generated by itself.

The consequence of not specifying a serialVersionUID is that when you add or modify any fields in a class, the serialized class cannot be recovered because the serialVersionUID generated by the new class and the old serialized object will be different. The process of Java serialization depends on the correct state of the serialized object recovery, and raises an invalid class exception of java.io.InvalidClassException when the serialized object sequence version does not match.

For example, you can see:

We keep the previously saved serialization file unchanged, and then modify the User class.

class User implements Serializable {
    public static String username;
    private transient String passwd;
    private String age;

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPasswd() {
        return passwd;
    }

    public void setPasswd(String passwd) {
        this.passwd = passwd;
    }

    public String getAge() {
        return age;
    }

    public void setAge(String age) {
        this.age = age;
    }
}

Add a property age, and then write another deserialization method:

public static void main(String[] args) {
        try {
            ObjectInputStream is = new ObjectInputStream(new FileInputStream(
                    "/Users/admin/Desktop/test/user.txt"));
            User user = (User) is.readObject(); // Read User's data from the stream
            is.close();

            System.out.println("\n modify User Data after class: ");
            System.out.println("username: " + user.getUsername());
            System.err.println("password: " + user.getPasswd());

        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }

Error is reported. We found that the serialVersionUID generated by the previous User class is different from the modified serialVersionUID (because it is generated by the hash code of the object), resulting in InvalidClassException exception.

Custom serialVersionUID:

class User implements Serializable {
    private static final long serialVersionUID = 4348344328769804325L;

    public static String username;
    private transient String passwd;
    private String age;

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPasswd() {
        return passwd;
    }

    public void setPasswd(String passwd) {
        this.passwd = passwd;
    }

    public String getAge() {
        return age;
    }

    public void setAge(String age) {
        this.age = age;
    }
}

Try again:

Data before serialization: 
username: hengheng
password: 123456

Data after serialization: 
username: Xiaoming
password: null

There is no error in the running result, so the serialVersionUID is generally customized.

Question 4: can you customize the serialization process?

The answer, of course, is yes.

We introduced the second way of serialization:

Implement the Externalizable interface, then override the writeExternal() and readExternal() methods, so you can customize the serialization.

For example, we try to set variables as transients.

public class ExternalizableTest implements Externalizable {

    private transient String content = "I am transient Decorated variables";

    @Override
    public void writeExternal(ObjectOutput out) throws IOException {
        out.writeObject(content);
    }

    @Override
    public void readExternal(ObjectInput in) throws IOException,
            ClassNotFoundException {
        content = (String) in.readObject();
    }

    public static void main(String[] args) throws Exception {

        ExternalizableTest et = new ExternalizableTest();
        ObjectOutput out = new ObjectOutputStream(new FileOutputStream(
                new File("test")));
        out.writeObject(et);

        ObjectInput in = new ObjectInputStream(new FileInputStream(new File(
                "test")));
        et = (ExternalizableTest) in.readObject();
        System.out.println(et.content);

        out.close();
        in.close();
    }
}

Operation result:

I'm a variable modified by transient

The implementation here is an Externalizable interface, so nothing can be serialized automatically. You need to manually specify the variables to be serialized in the writeExternal method, which has nothing to do with whether they are decorated by transient.

Do you know more about Java serialization?

Author: Yang Heng

Source: Yixin Institute of Technology

Tags: Programming Java network jvm Database

Posted on Tue, 10 Mar 2020 01:16:04 -0400 by alevsky