Serialization in Java. Not so simple
Serialization is a process that translates an object into a sequence of bytes, which can then be completely restored. Why do you need it?The fact is, with the usual program execution, the maximum lifetime of any object is known - from the launch of the program to its termination. Serialization allows you to expand this framework and “give life” to the object in the same way between program launches.
An added bonus to everything is the preservation of cross-platform. No matter what your operating system is, serialization translates the object into a stream of bytes, which can be restored to any OS. If you need to transfer an object over the network, you can serialize the object, save it to a file and transfer it over the network to the recipient. He will be able to recover the received object. Also, serialization allows remote calling of methods (Java RMI), which are located on different machines with possibly different operating systems, and work with them as if they are located on the machine of the calling java-process.
Implementing the serialization mechanism is quite simple. It is necessary for your class to implement the Serializable interface.. This interface is an identifier that has no methods, but it indicates jvm that objects of this class can be serialized. Since the serialization mechanism is associated with the basic input / output system and translates the object into a stream of bytes, to execute it, you must create an output OutputStream , package it in an ObjectOutputStream and call the writeObject () method . To restore an object, you need to package the InputStream into an ObjectInputStream and call the readObject () method .
In the process of serialization, its object graph is preserved along with the object being serialized. Those. all objects associated with this, objects of other classes will also be serialized with it.
Consider an example of serializing an object of class Person.
import java.io.*;
classHomeimplementsSerializable{
private String home;
publicHome(String home){
this.home = home;
}
public String getHome(){
return home;
}
}
publicclassPersonimplementsSerializable{
private String name;
privateint countOfNiva;
private String fatherName;
private Home home;
publicPerson(String name, int countOfNiva, String fatherName, Home home){
this.name = name;
this.countOfNiva = countOfNiva;
this.fatherName = fatherName;
this.home = home;
}
@Overridepublic String toString(){
return"Person{" +
"name='" + name + '\'' +
", countOfNiva=" + countOfNiva +
", fatherName='" + fatherName + '\'' +
", home=" + home +
'}';
}
publicstaticvoidmain(String[] args)throws IOException, ClassNotFoundException {
Home home = new Home("Vishnevaia 1");
Person igor = new Person("Igor", 2, "Raphael", home);
Person renat = new Person("Renat", 2, "Raphael", home);
//Сериализация в файл с помощью класса ObjectOutputStream
ObjectOutputStream objectOutputStream = new ObjectOutputStream(
new FileOutputStream("person.out"));
objectOutputStream.writeObject(igor);
objectOutputStream.writeObject(renat);
objectOutputStream.close();
// Востановление из файла с помощью класса ObjectInputStream
ObjectInputStream objectInputStream = new ObjectInputStream(
new FileInputStream("person.out"));
Person igorRestored = (Person) objectInputStream.readObject();
Person renatRestored = (Person) objectInputStream.readObject();
objectInputStream.close();
//Сериализация с помощью класса ByteArrayOutputStream
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream2 = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream2.writeObject(igor);
objectOutputStream2.writeObject(renat);
objectOutputStream2.flush();
//Восстановление с помощью класса ByteArrayInputStream
ObjectInputStream objectInputStream2 = new ObjectInputStream(
new ByteArrayInputStream(byteArrayOutputStream.toByteArray()));
Person igorRestoredFromByte = (Person) objectInputStream2.readObject();
Person renatRestoredFromByte = (Person) objectInputStream2.readObject();
objectInputStream2.close();
System.out.println("Before Serialize: " + "\n" + igor + "\n" + renat);
System.out.println("After Restored From Byte: " + "\n" + igorRestoredFromByte + "\n" + renatRestoredFromByte);
System.out.println("After Restored: " + "\n" + igorRestored + "\n" + renatRestored);
}
}
Conclusion:
Before Serialize:
Person{name='Igor', countOfNiva=2, fatherName='Raphael', home=Home@355da254}
Person{name='Renat', countOfNiva=2, fatherName='Raphael', home=Home@355da254}
After Restored From Byte:
Person{name='Igor', countOfNiva=2, fatherName='Raphael', home=Home@27973e9b}
Person{name='Renat', countOfNiva=2, fatherName='Raphael', home=Home@27973e9b}
After Restored:
Person{name='Igor', countOfNiva=2, fatherName='Raphael', home=Home@312b1dae}
Person{name='Renat', countOfNiva=2, fatherName='Raphael', home=Home@312b1dae}
In this example, the Home class was created to demonstrate that when the Person object is serialized, the graph of its objects is also serialized with it. The Home class must also implement the Serializable interface , otherwise a java.io.NotSerializableException will occur . The example also describes serialization using the class ByteArrayOutputStream .
An interesting conclusion can be made from the results of the program execution: when restoring objects that had a reference to the same object before serialization, this object will be restored only once . This can be seen from the same links in the objects after restoration:
After Restored From Byte:
Person{name='Igor', countOfNiva=2, fatherName='Raphael', home=Home@27973e9b}
Person{name='Renat', countOfNiva=2, fatherName='Raphael', home=Home@27973e9b}
After Restored:
Person{name='Igor', countOfNiva=2, fatherName='Raphael', home=Home@312b1dae}
Person{name='Renat', countOfNiva=2, fatherName='Raphael', home=Home@312b1dae}
However, it is also clear that when recording is performed by two output streams (we have an ObjectInputStream and ByteArrayOutputStream ), the home object will be recreated, despite the fact that it has already been created before in one of the streams. We see this at different addresses of home objects , received in two streams. It turns out that if you serialize one output stream, then restore the object, then we have a guarantee to restore the full network of objects without unnecessary duplicates. Of course, during the execution of the program the state of the objects may change, but this is on the programmer’s conscience.
Problem
From the example, it is also clear that when restoring an object, a ClassNotFoundException may occur. . What is the reason? The fact is that we can easily serialize an object of class Person into a file, transfer it over the network to our friend, who can restore the object to another application, in which Person simply does not exist.
Its serialization. How to do?
What if you want to manage the serialization yourself? For example, your object stores the username and password of users. You need to serialize it for further transmission over the network. Passing the password in this case is extremely unreliable. How to solve this problem? There are two ways. First, use the transient keyword . Second, instead of implementing the Serializable interest, use its extension - Externalizable interface. Consider the examples of the first and second methods for comparing them.
The first is Serialization using transient
import java.io.*;
publicclassLogonimplementsSerializable{
private String login;
privatetransient String password;
publicLogon(String login, String password){
this.login = login;
this.password = password;
}
@Overridepublic String toString(){
return"Logon{" +
"login='" + login + '\'' +
", password='" + password + '\'' +
'}';
}
publicstaticvoidmain(String[] args)throws IOException, ClassNotFoundException {
Logon igor = new Logon("IgorIvanovich", "Khoziain");
Logon renat = new Logon("Renat", "2500RUB");
System.out.println("Before: \n" + igor);
System.out.println(renat);
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("Externals.out"));
out.writeObject(igor);
out.writeObject(renat);
out.close();
ObjectInputStream in = new ObjectInputStream(new FileInputStream("Externals.out"));
igor = (Logon) in.readObject();
renat = (Logon) in.readObject();
System.out.println("After: \n" + igor);
System.out.println(renat);
}
}
Conclusion:
Before:
Logon{login='IgorIvanovich', password='Khoziain'}
Logon{login='Renat', password='2500RUB'}
After:
Logon{login='IgorIvanovich', password='null'}
Logon{login='Renat', password='null'}
The second way - Serialization with the implementation of the Externalizable interface
import java.io.*;
publicclassLogonimplementsExternalizable{
private String login;
private String password;
publicLogon(){
}
publicLogon(String login, String password){
this.login = login;
this.password = password;
}
@OverridepublicvoidwriteExternal(ObjectOutput out)throws IOException {
out.writeObject(login);
}
@Overridepublic String toString(){
return"Logon{" +
"login='" + login + '\'' +
", password='" + password + '\'' +
'}';
}
@OverridepublicvoidreadExternal(ObjectInput in)throws IOException, ClassNotFoundException {
login = (String) in.readObject();
}
publicstaticvoidmain(String[] args)throws IOException, ClassNotFoundException {
Logon igor = new Logon("IgorIvanovich", "Khoziain");
Logon renat = new Logon("Renat", "2500RUB");
System.out.println("Before: \n" + igor);
System.out.println(renat);
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("Externals.out"));
out.writeObject(igor);
out.writeObject(renat);
out.close();
ObjectInputStream in = new ObjectInputStream(new FileInputStream("Externals.out"));
igor = (Logon) in.readObject();
renat = (Logon) in.readObject();
System.out.println("After: \n" + igor);
System.out.println(renat);
}
}
Conclusion:
Before:
Logon{login='IgorIvanovich', password='Khoziain'}
Logon{login='Renat', password='2500RUB'}
After:
Logon{login='IgorIvanovich', password='null'}
Logon{login='Renat', password='null'}
The first difference between the two options that catches your eye is the size of the code. When implementing the Externalizable interface, we need to override two methods: writeExternal () and readExternal () . In the writeExternal () method, we specify which fields will be serialized and how, in readExternal (), how to read them. When using the word transient, we explicitly indicate which field or fields do not need to be serialized. Also note that in the second method we explicitly created a default constructor, and a public one. Why is this done? Let's try to run the code without this constructor. And look at the conclusion:
Before:
Logon{login='IgorIvanovich', password='Khoziain'}
Logon{login='Renat', password='2500RUB'}
Exception in thread "main" java.io.InvalidClassException: Logon; no valid constructor
at java.io.ObjectStreamClass$ExceptionInfo.newInvalidClassException(ObjectStreamClass.java:169)
at java.io.ObjectStreamClass.checkDeserialize(ObjectStreamClass.java:874)
at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2043)
at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1573)
at java.io.ObjectInputStream.readObject(ObjectInputStream.java:431)
at Logon.main(Logon.java:45)
We received an exception java.io.InvalidClassException . What is the reason? If you go through the stack, you can find out that the constructor of the ObjectStreamClass class contains the lines:
if (externalizable) {
cons = getExternalizableConstructor(cl);
} else {
cons = getSerializableConstructor(cl);
For the Externalizable interface, the getExternalizableConstructor () constructor get method will be called , inside which we will use Reflection to get the default constructor of the class for which we restore the object. If we are unable to find him, or he is not public , then we get an exception. You can bypass this situation as follows: do not explicitly create any constructor in the class and fill in the fields with setters and get the value of the getters. Then when the class is compiled, a default constructor will be created, which will be available to getExternalizableConstructor () . For Serializable getSerializableConstructor () method receives the constructor of the class Object and searches for the necessary class from it; if it does not find it, we get the exception ClassNotFoundException . It turns out that the key difference between Serializable and Externalizable is that the former does not need a constructor to create a recovery object. It simply recovers completely from bytes. For the second, during restoration, an object will first be created using a constructor at the declaration point, and then the values of its fields from the bytes received during serialization will be written to it. Personally, I prefer the first method, it is much easier. Moreover, even if we still need to set the serialization behavior, we can not use Externalizable , as well as implementSerializable by adding (without overriding) the writeObject () and readObject () methods to it . But in order for them to "work" you need to accurately observe their signature.
import java.io.*;
publicclassTaldaimplementsSerializable{
private String name;
private String description;
publicTalda(String name, String description){
this.name = name;
this.description = description;
}
privatevoidwriteObject(ObjectOutputStream stream)throws IOException {
stream.defaultWriteObject();
System.out.println("Our writeObject");
}
privatevoidreadObject(ObjectInputStream stream)throws IOException, ClassNotFoundException {
stream.defaultReadObject();
System.out.println("Our readObject");
}
@Overridepublic String toString(){
return"Talda{" +
"name='" + name + '\'' +
", description='" + description + '\'' +
'}';
}
publicstaticvoidmain(String[] args)throws IOException, ClassNotFoundException {
Talda partizanka = new Talda("Partizanka", "Viiiski");
System.out.println("Before: \n" + partizanka);
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("Talda.out"));
out.writeObject(partizanka);
out.close();
ObjectInputStream in = new ObjectInputStream(new FileInputStream("Talda.out"));
partizanka = (Talda) in.readObject();
System.out.println("After: \n" + partizanka);
}
}
Conclusion:
Before:
Talda{name='Partizanka', description='Viiiski'}
Our writeObject
Our readObject
After:
Talda{name='Partizanka', description='Viiiski'}
Inside our added methods, defaultWriteObject () and defaultReadObject () are called. They are responsible for the default serialization, as if it worked without the methods we added.
In fact, this is only the tip of the iceberg, if we continue to delve into the serialization mechanism, then with a high probability we can find some more nuances, finding which we will say: "Serialization ... not everything is so simple."