Details the serialization of objects in Java programming

  • 2020-04-01 04:25:22
  • OfStack

1. What is Java object serialization

The Java platform allows us to create reusable Java objects in memory, but in general, these objects are only possible when the JVM is running; that is, these objects do not live longer than the JVM. In a real-world application, however, you might be required to be able to save (persist) the specified object after the JVM is down and re-read the saved object in the future. Java object serialization can help us achieve this.

With Java object serialization, when an object is saved, its state is saved as a set of bytes that are later assembled into objects. It is important to note that object serialization holds the "state" of the object, its member variables. Thus, object serialization does not care about static variables in the class.

In addition to object serialization when persisting objects, object serialization is used when using RMI(remote method calls) or when passing objects across the network. The Java serialization API, which provides a standard mechanism for handling object serialization, is easy to use and will be covered in later sections of this article.

2. Simple example

In Java, a class can be serialized as long as it implements the java.io.Serializable interface. Here you will create a serializable class named Person, and all the examples in this article will revolve around that class or a modified version of it.

The Gender class, which is an enumerated type, represents Gender


public enum Gender { 
  MALE, FEMALE 
} 

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

The Person class, which implements the Serializable interface, contains three fields: name, type String; Age, Integer; Gender, gender type. In addition, you override the toString() method of the class to make it easier to print the contents of the Person instance.


public class Person implements Serializable { 
 
  private String name = null; 
 
  private Integer age = null; 
 
  private Gender gender = null; 
 
  public Person() { 
    System.out.println("none-arg constructor"); 
  } 
 
  public Person(String name, Integer age, Gender gender) { 
    System.out.println("arg constructor"); 
    this.name = name; 
    this.age = age; 
    this.gender = gender; 
  } 
 
  public String getName() { 
    return name; 
  } 
 
  public void setName(String name) { 
    this.name = name; 
  } 
 
  public Integer getAge() { 
    return age; 
  } 
 
  public void setAge(Integer age) { 
    this.age = age; 
  } 
 
  public Gender getGender() { 
    return gender; 
  } 
 
  public void setGender(Gender gender) { 
    this.gender = gender; 
  } 
 
  @Override 
  public String toString() { 
    return "[" + name + ", " + age + ", " + gender + "]"; 
  } 
} 

SimpleSerial, a simple serializer, saves a Person object to the file person.out, then reads the stored Person object from the file and prints it.


public class SimpleSerial { 
 
  public static void main(String[] args) throws Exception { 
    File file = new File("person.out"); 
 
    ObjectOutputStream oout = new ObjectOutputStream(new FileOutputStream(file)); 
    Person person = new Person("John", 101, Gender.MALE); 
    oout.writeObject(person); 
    oout.close(); 
 
    ObjectInputStream oin = new ObjectInputStream(new FileInputStream(file)); 
    Object newPerson = oin.readObject(); //There is no cast to the Person type
    oin.close(); 
    System.out.println(newPerson); 
  } 
} 

The output result of the above program is:


arg constructor 
[John, 31, MALE] 

It is important to note that when the saved Person object is re-read, none of the constructors of Person are called, and it looks as if the Person object was directly restored using bytes.

After the Person object is saved to the person.out file, we can read the file elsewhere to restore the object, but we must make sure that the reader's CLASSPATH contains the person.class (even if the Person class is not explicitly used when reading the Person object, as in the example above), or else it throws a ClassNotFoundException.

3. The role of Serializable

Why is it that a class that implements the Serializable interface can be serialized? In the example in the previous section, the ObjectOutputStream is used to persist the object, with the following code in the class:


private void writeObject0(Object obj, boolean unshared) throws IOException { 
   ...
  if (obj instanceof String) { 
    writeString((String) obj, unshared); 
  } else if (cl.isArray()) { 
    writeArray(obj, desc, unshared); 
  } else if (obj instanceof Enum) { 
    writeEnum((Enum) obj, desc, unshared); 
  } else if (obj instanceof Serializable) { 
    writeOrdinaryObject(obj, desc, unshared); 
  } else { 
    if (extendedDebugInfo) { 
      throw new NotSerializableException(cl.getName() + "n" 
          + debugInfoStack.toString()); 
    } else { 
      throw new NotSerializableException(cl.getName()); 
    } 
  } 
  ... 
} 

As you can see from the code above, if the type of the object being written is String, or array, or Enum, or Serializable, you can serialize the object, otherwise a NotSerializableException will be thrown.

4. Default serialization mechanism

If you simply have a class implement the Serializable interface without any other processing, you are using the default serialization mechanism. Using the default mechanism, when an object is serialized, not only is the current object serialized, but other objects referenced by that object are serialized, as are other objects referenced by those other objects, and so on. So, if an object contains container-class objects whose member variables are container-class objects and whose elements are container-class objects, the serialization process is more complicated and expensive.

5. Affects serialization

In real-world applications, the default serialization mechanism is sometimes not available. For example, you want to ignore sensitive data during serialization or simplify the serialization process. Several ways to affect serialization are described below.

5.1 transient keyword

When a field is declared transient, the default serialization mechanism ignores the field. Declare the age field in the Person class as transient, as shown below,


public class Person implements Serializable { 
  ... 
  transient private Integer age = null; 
  ... 
} 

When the SimpleSerial application is executed, the following output is produced:


arg constructor 
[John, null, MALE] 

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

5.2 methods writeObject() and readObject(

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


public class Person implements Serializable { 
  ... 
  transient private Integer age = null; 
  ... 
 
  private void writeObject(ObjectOutputStream out) throws IOException { 
    out.defaultWriteObject(); 
    out.writeInt(age); 
  } 
 
  private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { 
    in.defaultReadObject(); 
    age = in.readInt(); 
  } 
} 

The defaultWriteObject() method in the ObjectOutputStream is called first in the writeObject() method, which performs the default serialization mechanism, ignoring the age field as described in section 5.1. Then call the writeInt() method to explicitly write the age field to the ObjectOutputStream. ReadObject () is used to read objects, the same way as the writeObject() method. A second execution of the SimpleSerial application results in the following output:


arg constructor 
[John, 31, MALE] 

It is important to note that writeObject() and readObject() are both private methods, so how are they called? Use reflection, no doubt. See the writeSerialData method in the ObjectOutputStream and the readSerialData method in the ObjectInputStream for details.

5.3 Externalizable interface

Both using the transient keyword and using the writeObject() and readObject() methods are Serializable based on the Serializable interface. Another serialization interface, Externalizable, is provided in the JDK, with which the previous serialization mechanism based on the Serializable interface is disabled. At this point, the Person class is modified as follows,


public class Person implements Externalizable { 
 
  private String name = null; 
 
  transient private Integer age = null; 
 
  private Gender gender = null; 
 
  public Person() { 
    System.out.println("none-arg constructor"); 
  } 
 
  public Person(String name, Integer age, Gender gender) { 
    System.out.println("arg constructor"); 
    this.name = name; 
    this.age = age; 
    this.gender = gender; 
  } 
 
  private void writeObject(ObjectOutputStream out) throws IOException { 
    out.defaultWriteObject(); 
    out.writeInt(age); 
  } 
 
  private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { 
    in.defaultReadObject(); 
    age = in.readInt(); 
  } 
 
  @Override 
  public void writeExternal(ObjectOutput out) throws IOException { 
 
  } 
 
  @Override 
  public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException { 
 
  } 
  ... 
} 

When the SimpleSerial program is executed, the following results are obtained:


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

From this result, on the one hand, you can see that none of the fields in the Person object are serialized. On the other hand, if you're careful, you can see that this serialization procedure calls the parameter-free constructor of the Person class.

Externalizable inherits from Serializable, and when the interface is used, the details of serialization need to be done by the programmer. As shown above, since the writeExternal() and readExternal() methods do nothing, this serialization behavior will not save/read any of the fields. This is why all the fields in the output are null.

In addition, when serializing with Externalizable, when an object is read, the parameterless 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 parameter free constructor of the Person class is called during this serialization. For this reason, a class that implements the Externalizable interface must provide a no-argument constructor with access to public.

The above Person class is further modified to serialize the name and age fields, but the gender field is ignored, as shown below:


public class Person implements Externalizable { 
 
  private String name = null; 
 
  transient private Integer age = null; 
 
  private Gender gender = null; 
 
  public Person() { 
    System.out.println("none-arg constructor"); 
  } 
 
  public Person(String name, Integer age, Gender gender) { 
    System.out.println("arg constructor"); 
    this.name = name; 
    this.age = age; 
    this.gender = gender; 
  } 
 
  private void writeObject(ObjectOutputStream out) throws IOException { 
    out.defaultWriteObject(); 
    out.writeInt(age); 
  } 
 
  private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { 
    in.defaultReadObject(); 
    age = in.readInt(); 
  } 
 
  @Override 
  public void writeExternal(ObjectOutput out) throws IOException { 
    out.writeObject(name); 
    out.writeInt(age); 
  } 
 
  @Override 
  public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException { 
    name = (String) in.readObject(); 
    age = in.readInt(); 
  } 
  ... 
} 

After executing SimpleSerial, the following results are obtained:


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

5.4 readResolve () method

When we use the Singleton pattern, we should expect an instance of a class to be unique, but if the class is serializable, the situation might be slightly different. At this point, the Person class used in section 2 is modified to implement the Singleton pattern, as shown below:


public class Person implements Serializable { 
 
  private static class InstanceHolder { 
    private static final Person instatnce = new Person("John", 31, Gender.MALE); 
  } 
 
  public static Person getInstance() { 
    return InstanceHolder.instatnce; 
  } 
 
  private String name = null; 
 
  private Integer age = null; 
 
  private Gender gender = null; 
 
  private Person() { 
    System.out.println("none-arg constructor"); 
  } 
 
  private Person(String name, Integer age, Gender gender) { 
    System.out.println("arg constructor"); 
    this.name = name; 
    this.age = age; 
    this.gender = gender; 
  } 
  ... 
} 
 And also modify SimpleSerial Apply so that it can be saved / Get the above singleton object and make an object equality comparison, as shown in the following code: 

public class SimpleSerial { 
 
  public static void main(String[] args) throws Exception { 
    File file = new File("person.out"); 
    ObjectOutputStream oout = new ObjectOutputStream(new FileOutputStream(file)); 
    oout.writeObject(Person.getInstance()); //Save the singleton object
    oout.close(); 
 
    ObjectInputStream oin = new ObjectInputStream(new FileInputStream(file)); 
    Object newPerson = oin.readObject(); 
    oin.close(); 
    System.out.println(newPerson); 
 
    System.out.println(Person.getInstance() == newPerson); //Compare the obtained object to the singleton object in the Person class for equality
  } 
} 

After executing the above application, the following results are obtained:


arg constructor 
[John, 31, MALE] 
false 

It is worth noting that the person object retrieved from the file person.out is not the same as the singleton object in the person class. To preserve the singleton during serialization, add a readResolve() method to the Person class that returns the singleton of the Person directly, as shown below:


public class Person implements Serializable { 
 
  private static class InstanceHolder { 
    private static final Person instatnce = new Person("John", 31, Gender.MALE); 
  } 
 
  public static Person getInstance() { 
    return InstanceHolder.instatnce; 
  } 
 
  private String name = null; 
 
  private Integer age = null; 
 
  private Gender gender = null; 
 
  private Person() { 
    System.out.println("none-arg constructor"); 
  } 
 
  private Person(String name, Integer age, Gender gender) { 
    System.out.println("arg constructor"); 
    this.name = name; 
    this.age = age; 
    this.gender = gender; 
  } 
 
  private Object readResolve() throws ObjectStreamException { 
    return InstanceHolder.instatnce; 
  } 
  ... 
} 

When the SimpleSerial application of this section is executed again, the following output is produced:


arg constructor 
[John, 31, MALE] 
true 

Whether you implement the Serializable interface or the Externalizable interface, the readResolve() method is called when an object is read from an I/O stream. In effect, you simply replace the object created during the deserialization process with the object returned from readResolve().

6. Some advanced usage
Everything is said in the notes. Just give it to the program.


package test.javaPuzzler.p5;

import java.io.*;
import java.io.ObjectInputStream.GetField;
import java.io.ObjectOutputStream.PutField;

//A class implements Serializable to indicate that it can be serialized;
//One thing to note:
//If a subclass implements Serializable and the parent class does not, the parent class is not serialized.
public class SerializableObject implements Serializable {

 //The resulting serialization version number will vary depending on the compilation environment, declared class name, member name, and number of changes.
 //In other words, this version number to some extent records the class definition information, if the class definition changes, it is better to regenerate the version number;
 //If the new code USES the old version number, it can read the bytecode of the old class during deserialization without error.
 private static final long serialVersionUID = 9038542591452547920L;

 public String name;
 public String password;
 //If you don't want a non-static member to be serialized, consider it transient.
 public transient int age;
 //Static members are not serialized because the serialization holds the state information of the instance, while the static members are the state information of the class.
 public static int version = 1;

 public SerializableObject(String name, String password) {
 this.name = name;
 this.password = password;
 }

 //Each class can write a writeObject method, which is responsible for serializing the class itself.
 //For example, for sensitive information like password, it can be encrypted and then serialized;
 //This process requires PutField, which specifies which fields will be serialized and how (e.g. encrypted);
 //If this method is not defined, the defaultWriteObject of the ObjectOutputStream will be called;

 //You can comment out the readObject method and run the test case to see if the password is encrypted.
 private void writeObject(ObjectOutputStream out) throws IOException {
 PutField putFields = out.putFields();
 putFields.put("name", name);
 //Simulated encrypted password
 putFields.put("password", "thePassword:" + password);
 out.writeFields();
 }

 //Each class can write a readObject method, which is responsible for the deserialization of the class itself.
 //For example, decrypt the encrypted password during serialization;
 //This process requires the use of GetField, which can specifically read each field; Or perform decryption actions, etc.
 //If this method is not defined, the defaultReadObject of ObjectInputStream will be called;
 private void readObject(ObjectInputStream in)
  throws ClassNotFoundException, IOException {
 GetField readFields = in.readFields();
 //After reading the value of the member, directly assign to the field, that is, complete the deserialization of the field;
 name = (String) readFields.get("name", "defaultName");
 //Simulated decryption cipher
 String encPassword = (String) readFields.get("password",
  "thePassword:defaultValue");
 password = encPassword.split(":")[1];
 }

 //serialization
 //I'm going to use ObjectOutputStream;
 public void save() throws IOException {
 FileOutputStream fout = new FileOutputStream("e:\obj");
 ObjectOutputStream oout = new ObjectOutputStream(fout);
 oout.writeObject(this);
 oout.close();
 fout.close();
 }

 //deserialization
 //I'm going to use ObjectInputStream
 public static SerializableObject load() throws IOException,
  ClassNotFoundException {
 FileInputStream fin = new FileInputStream("e:\obj");
 ObjectInputStream oin = new ObjectInputStream(fin);
 Object o = oin.readObject();
 return (SerializableObject) o;

 }

 @Override
 public String toString() {
 return "name: " + name + ", password: " + password;
 }

 //The test case
 public static void main(String[] args) throws IOException,
  ClassNotFoundException {
 SerializableObject so = new SerializableObject(
  "http://blog.csdn.net/sunxing007", "123456");
 so.save();
 System.out.println(so);
 System.out.println(SerializableObject.load());
 }

}

Serialization is bad for the singleton pattern because you can break the singleton by deserializing it.


public class Dog extends Exception {
 //private static final long serialVersionUID = -7156412195888553079L;
 public static final Dog INSTANCE = new Dog();
 private Dog() { }
 public String toString() {
 return "Woof";
 }
 //With readResolve, the returned object is guaranteed to be fully autonomous when deserialized.
 private Object readResolve(){
      return INSTANCE;
    }
 public static void main(String[] args) throws IOException, ClassNotFoundException{
 Dog d = Dog.INSTANCE;
 ByteArrayOutputStream bro = new ByteArrayOutputStream();
 ObjectOutputStream oout = new ObjectOutputStream(bro);
 oout.writeObject(d);
 ObjectInputStream oin = new ObjectInputStream(new ByteArrayInputStream(bro.toByteArray()));
 Dog d1 = (Dog)oin.readObject();
 System.out.println(d1==d);
 }
}


Related articles: