Understanding Java Object Serialization

Keywords: Java jvm network JDK

Understanding Java object serialization

Links to the original text: http://www.blogjava.net/jiangshachina/archive/2012/02/13/369898.html

 

There are many articles about Java serialization. This article is a summary of my personal past study, understanding and application of Java serialization. This article covers the basic principles of Java serialization and various methods for customizing serialization forms. When writing this article, I not only refer to the related articles and other network materials in Thinking in Java, Effective Java, Java World, developerWorks, but also add my own practical experience and understanding. I hope it will be helpful to you all.

1. What is Java object serialization
The Java platform allows us to create reusable Java objects in memory, but in general, these objects may exist only when the JVM is running, that is, the lifetime of these objects is not longer than that of the JVM. However, in practical applications, it may be required to save (persist) the specified objects after the JVM stops running, and to re-read the saved objects in the future. Java object serialization can help us achieve this function.
Using Java object serialization, when an object is saved, its state is saved as a set of bytes, which in the future will be assembled into objects. It must be noted that object serialization preserves the object's "state", that is, its member variables. As a result, object serialization does not focus on static variables in classes.
In addition to object serialization when persisting objects, object serialization is used when using RMI (remote method call) or when passing objects over the network. Java serialization API provides a standard mechanism for dealing with object serialization, which is easy to use and will be covered in subsequent chapters of this article.

2. Simple examples
In Java, as long as a class implements the java.io.Serializable interface, it can be serialized. A serializable Person class will be created here, and all the examples in this article will revolve around that class or its modified version.
Gender class, which is an enumeration type, denotes gender

1 public enum Gender {
2     MALE, FEMALE
3 }

If you are familiar with Java enumeration types, you should know that each enumeration type defaults to the inheritance class java.lang.Enum, which implements the Serializable interface, so enumeration type objects can be serialized by default.

Person class, implements the Serializable interface, which contains three fields: name, String type, age, Integer type, gender, Gender type. In addition, the toString() method of this class is overridden to facilitate printing the contents of Person instances.

 1 public class Person implements Serializable {
 2 
 3     private String name = null;
 4 
 5     private Integer age = null;
 6 
 7     private Gender gender = null;
 8 
 9     public Person() {
10         System.out.println("none-arg constructor");
11     }
12 
13     public Person(String name, Integer age, Gender gender) {
14         System.out.println("arg constructor");
15         this.name = name;
16         this.age = age;
17         this.gender = gender;
18     }
19 
20     public String getName() {
21         return name;
22     }
23 
24     public void setName(String name) {
25         this.name = name;
26     }
27 
28     public Integer getAge() {
29         return age;
30     }
31 
32     public void setAge(Integer age) {
33         this.age = age;
34     }
35 
36     public Gender getGender() {
37         return gender;
38     }
39 
40     public void setGender(Gender gender) {
41         this.gender = gender;
42     }
43 
44     @Override
45     public String toString() {
46         return "[" + name + ", " + age + ", " + gender + "]";
47     }
48 }

 

SimpleSerial, a simple serialization program, first saves a Person object to the file person.out, then reads the stored Person object from the file and prints the object.

 
 1 public class SimpleSerial {
 2 
 3     public static void main(String[] args) throws Exception {
 4         File file = new File("person.out");
 5 
 6         ObjectOutputStream oout = new ObjectOutputStream(new FileOutputStream(file));
 7         Person person = new Person("John", 101, Gender.MALE);
 8         oout.writeObject(person);
 9         oout.close();
10 
11         ObjectInputStream oin = new ObjectInputStream(new FileInputStream(file));
12         Object newPerson = oin.readObject(); // No mandatory conversion to Person type
13         oin.close();
14         System.out.println(newPerson);
15     }
16 }

The output of the above procedure is as follows:

arg constructor
[John, 31, MALE]

It must be noted that when the saved Person object is re-read, no constructor of Person is invoked, which looks like a direct use of bytes to restore the Person object.

When the Person object is saved in the person.out file, we can read it elsewhere to restore the object, but we must ensure that the CLASSPATH of the reader contains Person.class (even if the Person class is not used explicitly when reading the Person object, as shown in the example above), otherwise ClassNotFoundException will be thrown.

3. The role of Serializable
Why can a class be serialized if it implements the Serializable interface? In the example in the previous section, ObjectOutputStream is used to persist objects, in which the following code is available:

 1 private void writeObject0(Object obj, boolean unshared) throws IOException {
 2     
 3     if (obj instanceof String) {
 4         writeString((String) obj, unshared);
 5     } else if (cl.isArray()) {
 6         writeArray(obj, desc, unshared);
 7     } else if (obj instanceof Enum) {
 8         writeEnum((Enum) obj, desc, unshared);
 9     } else if (obj instanceof Serializable) {
10         writeOrdinaryObject(obj, desc, unshared);
11     } else {
12         if (extendedDebugInfo) {
13             throw new NotSerializableException(cl.getName() + "\n"
14                     + debugInfoStack.toString());
15         } else {
16             throw new NotSerializableException(cl.getName());
17         }
18     }
19     
20 }

From the above code, if the type of the object being written is String, or array, or Enum, or Serializable, then the object can be serialized, otherwise NotSerializable Exception will be thrown.


4. Default serialization mechanism
If only one class implements the Serializable interface without any other processing, the default serialization mechanism is used. Using the default mechanism, when serializing an object, it not only serializes the current object itself, but also serializes other objects referenced by that object. Similarly, other objects referenced by these other objects will be serialized, and so on. Therefore, if the member variables contained in an object are container class objects, and the elements contained in these containers are container class objects, the serialization process will be more complex and expensive.

5. Impact serialization
In practical applications, sometimes the default serialization mechanism cannot be used. For example, you want to ignore sensitive data in the serialization process, or simplify the serialization process. Several ways of influencing serialization are described below.

5.1 transient keyword
When a field is declared transiently, the default serialization mechanism ignores the field. The age field in the Person class is declared transient here, as shown below.

1 public class Person implements Serializable {
2     
3     transient private Integer age = null;
4     
5 }

To execute the SimpleSerial application, the following output will be generated:

arg constructor
[John, null, MALE]

As you can see, the age field is not serialized.


5.2 WritteObject () method and readObject() method
For the field age declared as transitive, is there any other way to make it serializable again besides removing the transitive keyword? One way is to add two methods to the Person class: writeObject() and readObject(), as follows:

 1 public class Person implements Serializable {
 2     
 3     transient private Integer age = null;
 4     
 5 
 6     private void writeObject(ObjectOutputStream out) throws IOException {
 7         out.defaultWriteObject();
 8         out.writeInt(age);
 9     }
10 
11     private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
12         in.defaultReadObject();
13         age = in.readInt();
14     }
15 }

The defaultWriteObject() method in ObjectOutputStream is first called in the writeObject() method, which performs the default serialization mechanism, as described in Section 5.1, and ignores the age field. Then the writeInt() method is called to write the age field to ObjectOutputStream visually. The function of readObject() is to read objects, and its principle is the same as that of writeObject().

If the SimpleSerial application is executed again, the following output will be generated:

arg constructor
[John, 31, MALE]

It must be noted that both writeObject() and readObject() are private methods, so how are they called? There is no doubt that reflection is used. Details can be found in the writeSerialData method in ObjectOutputStream and the readSerialData method in ObjectInputStream.


5.3 Externalizable interface
Whether you use the transient keyword or the writeObject() and readObject() methods, they are all serializations based on the Serializable interface. Another serialization interface, Externalizable, is provided in JDK. With this interface, the previous serialization mechanism based on Serializable interface will fail. Modify the Person class to read as follows.

 1 public class Person implements Externalizable {
 2 
 3     private String name = null;
 4 
 5     transient private Integer age = null;
 6 
 7     private Gender gender = null;
 8 
 9     public Person() {
10         System.out.println("none-arg constructor");
11     }
12 
13     public Person(String name, Integer age, Gender gender) {
14         System.out.println("arg constructor");
15         this.name = name;
16         this.age = age;
17         this.gender = gender;
18     }
19 
20     private void writeObject(ObjectOutputStream out) throws IOException {
21         out.defaultWriteObject();
22         out.writeInt(age);
23     }
24 
25     private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
26         in.defaultReadObject();
27         age = in.readInt();
28     }
29 
30     @Override
31     public void writeExternal(ObjectOutput out) throws IOException {
32 
33     }
34 
35     @Override
36     public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
37 
38     }
39     
40 }

After executing the SimpleSerial program at this time, the following results will be obtained:

arg constructor
none-arg constructor
[null, null, null]

From this result, on the one hand, it can be seen that none of the fields in the Person object has been serialized. On the other hand, if you are careful, you can also find that this serialization process calls the parametric constructor of the Person class.

Externalizable inherits from Serializable, and when using this interface, serialization details need to be done by programmers. As shown above, because the writeExternal() and readExternal() methods are not processed, the serialization behavior will not save / read any fields. This is why all fields in the output result have empty values.


In addition, if you use Externalizable for serialization, when reading an object, the parametric constructor of the serialized class is called to create a new object, and then the values of the fields of the saved object are filled into the new object separately. This is why the parametric constructor of the Person class is called during this serialization. For this reason, the class that implements the Externalizable interface must provide a parametric constructor with public access.
Make further modifications to the Person class to enable it to serialize the name and age fields, but ignore the gender field, as shown in the following code:

 1 public class Person implements Externalizable {
 2 
 3     private String name = null;
 4 
 5     transient private Integer age = null;
 6 
 7     private Gender gender = null;
 8 
 9     public Person() {
10         System.out.println("none-arg constructor");
11     }
12 
13     public Person(String name, Integer age, Gender gender) {
14         System.out.println("arg constructor");
15         this.name = name;
16         this.age = age;
17         this.gender = gender;
18     }
19 
20     private void writeObject(ObjectOutputStream out) throws IOException {
21         out.defaultWriteObject();
22         out.writeInt(age);
23     }
24 
25     private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
26         in.defaultReadObject();
27         age = in.readInt();
28     }
29 
30     @Override
31     public void writeExternal(ObjectOutput out) throws IOException {
32         out.writeObject(name);
33         out.writeInt(age);
34     }
35 
36     @Override
37     public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
38         name = (String) in.readObject();
39         age = in.readInt();
40     }
41     
42 }

After implementing SimpleSerial, the following results will be achieved:

arg constructor
none-arg constructor
[John, 31, null]

Note: Whether you are using the writeObject() and readObject() methods or the writeExternal() and readExternal() methods in the Externalizable interface to customize serialization, you need to pay attention to the order of the read () methods must be consistent with the order of the write () methods, otherwise reading errors will occur.

 

5.4 readResolve() method
When we use the Singleton pattern, we should expect that instances of a class should be unique, but if the class is serializable, the situation may be slightly different. At this point, the Person class used in Section 2 is modified to implement the Singleton pattern, as follows:

 1 public class Person implements Serializable {
 2 
 3     private static class InstanceHolder {
 4         private static final Person instatnce = new Person("John", 31, Gender.MALE);
 5     }
 6 
 7     public static Person getInstance() {
 8         return InstanceHolder.instatnce;
 9     }
10 
11     private String name = null;
12 
13     private Integer age = null;
14 
15     private Gender gender = null;
16 
17     private Person() {
18         System.out.println("none-arg constructor");
19     }
20 
21     private Person(String name, Integer age, Gender gender) {
22         System.out.println("arg constructor");
23         this.name = name;
24         this.age = age;
25         this.gender = gender;
26     }
27     
28 }

 

SimpleSerial applications need to be modified so that the above singleton objects can be saved/retrieved and the object equality comparison can be made, as shown in the following code:

 1 public class SimpleSerial {
 2 
 3     public static void main(String[] args) throws Exception {
 4         File file = new File("person.out");
 5         ObjectOutputStream oout = new ObjectOutputStream(new FileOutputStream(file));
 6         oout.writeObject(Person.getInstance()); // Save the singleton object
 7         oout.close();
 8 
 9         ObjectInputStream oin = new ObjectInputStream(new FileInputStream(file));
10         Object newPerson = oin.readObject();
11         oin.close();
12         System.out.println(newPerson);
13 
14         System.out.println(Person.getInstance() == newPerson); // The object to be acquired and Person Equivalence comparison of singleton objects in classes
15     }
16 }

After executing the above application, the following results will be obtained:

arg constructor
[John, 31, MALE]
false

It is noteworthy that the Person object obtained from the file person.out is not the same as the singleton object in the Person class. In order to preserve the singleton feature in the serialization process, a readResolve() method can be added to the Person class, in which the singleton object of Person can be returned directly, as follows:

 1 public class Person implements Serializable {
 2 
 3     private static class InstanceHolder {
 4         private static final Person instatnce = new Person("John", 31, Gender.MALE);
 5     }
 6 
 7     public static Person getInstance() {
 8         return InstanceHolder.instatnce;
 9     }
10 
11     private String name = null;
12 
13     private Integer age = null;
14 
15     private Gender gender = null;
16 
17     private Person() {
18         System.out.println("none-arg constructor");
19     }
20 
21     private Person(String name, Integer age, Gender gender) {
22         System.out.println("arg constructor");
23         this.name = name;
24         this.age = age;
25         this.gender = gender;
26     }
27 
28     private Object readResolve() throws ObjectStreamException {
29         return InstanceHolder.instatnce;
30     }
31     
32 }

When the SimpleSerial application in this section is executed again, the following output will be generated:

arg constructor
[John, 31, MALE]
true

Whether the Serializable interface is implemented or the Externalizable interface is implemented, the readResolve() method is called when an object is read from an I/O stream. In fact, the object returned in readResolve() directly replaces the object created in the deserialization process, and the created object is garbage collected.

Posted by NateDawg on Mon, 08 Apr 2019 14:36:32 -0700