📜 ⬆️ ⬇️

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 between the program launches as well.

An added bonus to everything is to save 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 resulting 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 from other classes will also be serialized with it.

Consider an example of serializing an object of class Person.

import java.io.*; class Home implements Serializable { private String home; public Home(String home) { this.home = home; } public String getHome() { return home; } } public class Person implements Serializable { private String name; private int countOfNiva; private String fatherName; private Home home; public Person(String name, int countOfNiva, String fatherName, Home home) { this.name = name; this.countOfNiva = countOfNiva; this.fatherName = fatherName; this.home = home; } @Override public String toString() { return "Person{" + "name='" + name + '\'' + ", countOfNiva=" + countOfNiva + ", fatherName='" + fatherName + '\'' + ", home=" + home + '}'; } public static void main(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 ByteArrayOutputStream class.

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 a program, the state of objects may change, but this is on the programmer’s conscience.

Problem

It is also seen from the example that when the object is restored, a ClassNotFoundException exception 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 have an object.

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. The second, instead of implementing the Serializable interest, is to use its extension, the Externalizable interface. Consider the examples of the first and second methods for comparing them.

The first is Serialization using transient

 import java.io.*; public class Logon implements Serializable { private String login; private transient String password; public Logon(String login, String password) { this.login = login; this.password = password; } @Override public String toString() { return "Logon{" + "login='" + login + '\'' + ", password='" + password + '\'' + '}'; } public static void main(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.*; public class Logon implements Externalizable { private String login; private String password; public Logon() { } public Logon(String login, String password) { this.login = login; this.password = password; } @Override public void writeExternal(ObjectOutput out) throws IOException { out.writeObject(login); } @Override public String toString() { return "Logon{" + "login='" + login + '\'' + ", password='" + password + '\'' + '}'; } @Override public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException { login = (String) in.readObject(); } public static void main(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 trace, you can find out that the constructor of the ObjectStreamClass class has the lines:

  if (externalizable) { cons = getExternalizableConstructor(cl); } else { cons = getSerializableConstructor(cl); 

For the Externalizable interface, we will call the getExternalizableConstructor () method for getting the constructor , 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 for getExternalizableConstructor () . For Serializable, the getSerializableConstructor () method receives the constructor of the Object class and searches for the required class from it. If it does not find it, we get the ClassNotFoundException exception. It turns out that the key difference between Serializable and Externalizable is that the former does not need a constructor to create an object recovery. 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 implement Serializable , by adding (without redefining) the writeObject () and readObject () methods into it. But in order for them to "work" you need to accurately observe their signature.

 import java.io.*; public class Talda implements Serializable { private String name; private String description; public Talda(String name, String description) { this.name = name; this.description = description; } private void writeObject(ObjectOutputStream stream) throws IOException { stream.defaultWriteObject(); System.out.println("Our writeObject"); } private void readObject(ObjectInputStream stream) throws IOException, ClassNotFoundException { stream.defaultReadObject(); System.out.println("Our readObject"); } @Override public String toString() { return "Talda{" + "name='" + name + '\'' + ", description='" + description + '\'' + '}'; } public static void main(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, we call defaultWriteObject () and defaultReadObject () . 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."

Source: https://habr.com/ru/post/431524/


All Articles