Hi! Today we'll continue to get to know serialization and deserialization of Java objects. In the last lesson, we got to know the Serializable marker interface, reviewed examples of its use, and also learned how you can use the transient keyword to control the serialization process. Well, saying that we 'control the process' may be overstating it. We have one keyword, one version identifier, and that's about it. The rest of the process is hidden inside Java, and we can't access it. Of course, in terms of convenience, this is good. But a programmer shouldn't only be guided by his or her own comfort, right? :) There are other factors that you need to consider. That's why Serializable isn't the only mechanism for serialization-deserialization in Java. Today we'll get acquainted with the Externalizable interface. But before we start studying it, you might have a reasonable question: why do we need another mechanism? Serializable did its job, and what's not to love about the automatic implementation of the whole process? And the examples we looked at were also uncomplicated. So what's the problem? Why do we need another interface for essentially the same tasks? The fact is that Serializable has several shortcomings. We list some of them:
  1. Performance. The Serializable interface has many advantages, but high performance is clearly not one of them.

    First, Serializable's internal implementation generates a large amount of service information and all sorts of temporary data.

    Second, Serializable relies on the Reflection API (you don't have to dive deep on this right now; you can read more at your leisure, if you're interested). This thing lets you do the seemingly impossible things in Java: for example, change the values of private fields. CodeGym has an excellent article about the Reflection API. You can read about it there.

  2. Flexibility. We don't control the serialization-deserialization process when we use the Serializable interface.

    One the one hand, it's very convenient, because if we aren't particularly concerned about performance, then it seems nice to not have to write code. But what if we really need to add some of our own features (we'll provide an example below) to the serialization logic?

    Basically, all we have to control the process is the transient keyword to exclude some data. That's it. That's our entire toolbox :/

  3. Security. This item derives in part from the previous item.

    We haven't spend much time thinking about this before, but what if some information in your class is not intended for others' prying eyes and ears? A simple example is a password or other personal user data, which in today's world are governed by a bunch of laws.

    If we use Serializable, we can't really do anything about it. We serialize everything as it is.

    But if we do it the right way, we must encrypt this kind of data before writing it to a file or sending it over a network. But Serializable doesn't make this possible.

Well, let's at long last see how the class would look if we use the Externalizable interface.
import java.io.Externalizable;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;

public class UserInfo implements Externalizable {

   private String firstName;
   private String lastName;
   private String superSecretInformation;

private static final long serialVersionUID = 1L;

   // ...constructor, getters, setters, toString()...

   @Override
   public void writeExternal(ObjectOutput out) throws IOException {

   }

   @Override
   public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {

   }
}
As you can see, we have significant changes! The main one is obvious: when implementing the Externalizable interface, you must implement two required methods: writeExternal() and readExternal(). As we said earlier, the responsibility for serialization and deserialization will lie with the programmer. But now you can solve the problem of no control over the process! The whole process is programmed directly by you. Naturally, this allows a much more flexible mechanism. Additionally, the problem with security is solved. As you can see, our class has a personal data field that cannot be stored unencrypted. Now we can easily write code that satisfies this constraint. For example, we can add to our class two simple private methods to encrypt and decrypt sensitive data. We will write the data to the file and read it from the file in encrypted form. The rest of the data will be written and read as it is :) As a result, our class looks something like this:
import java.io.Externalizable;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;
import java.util.Base64;

public class UserInfo implements Externalizable {

   private String firstName;
   private String lastName;
   private String superSecretInformation;

private static final long serialVersionUID = 1L;

   @Override
   public void writeExternal(ObjectOutput out) throws IOException {
       out.writeObject(this.getFirstName());
       out.writeObject(this.getLastName());
       out.writeObject(this.encryptString(this.getSuperSecretInformation()));
   }

   @Override
   public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
       firstName = (String) in.readObject();
       lastName = (String) in.readObject();
       superSecretInformation = this.decryptString((String) in.readObject());
   }

   private String encryptString(String data) {
       String encryptedData = Base64.getEncoder().encodeToString(data.getBytes());
       System.out.println(encryptedData);
       return encryptedData;
   }

   private String decryptString(String data) {
       String decrypted = new String(Base64.getDecoder().decode(data));
       System.out.println(decrypted);
       return decrypted;
   }

   public String getFirstName() {
       return firstName;
   }

   public String getLastName() {
       return lastName;
   }

   public String getSuperSecretInformation() {
       return superSecretInformation;
   }
}
We implemented two methods that use the same ObjectOutput and ObjectInput parameters that we already met in the lesson about Serializable. At the right moment, we encrypt or decrypt the required data, and we use the encrypted data to serialize our object. Let's see how this looks in practice:
import java.io.*;

public class Main {

   public static void main(String[] args) throws IOException, ClassNotFoundException {

       FileOutputStream fileOutputStream = new FileOutputStream("C:\\Users\\Username\\Desktop\\save.ser");
       ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);

       UserInfo userInfo = new UserInfo("Paul", "Piper", "Paul Piper's passport data");

       objectOutputStream.writeObject(userInfo);

       objectOutputStream.close();

   }
}
In the encryptString() and decryptString() methods, we specifically added console output to verify the form in which the secret data will be written and read. The code above displayed the following line: SXZhbiBJdmFub3YncyBwYXNzcG9ydCBkYXRh The encryption succeeded! The full contents of the file look like this: ¬н sr UserInfoГ!}ҐџC‚ћ xpt Ivant Ivanovt $SXZhbiBJdmFub3YncyBwYXNzcG9ydCBkYXRhx Now let's try using our deserialization logic.
public class Main {

   public static void main(String[] args) throws IOException, ClassNotFoundException {

       FileInputStream fileInputStream = new FileInputStream("C:\\Users\\Username\\Desktop\\save.ser");
       ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);


       UserInfo userInfo = (UserInfo) objectInputStream.readObject();
       System.out.println(userInfo);

       objectInputStream.close();

   }
}
Well, nothing seems complicated here. It should work! We run it and get... Exception in thread "main" java.io.InvalidClassException: UserInfo; no valid constructor Oops! :( Apparently, it's not so easy! The deserialization mechanism threw an exception and demanded that we create a default constructor. I wonder why. With Serializable, we got by without one... :/ Here we've encountered another important nuance. The difference between Serializable and Externalizable lies not only in the programmer's 'expanded' access and the ability to more flexibly control the process, but also in the process itself. Above all, in the deserialization mechanism. When using Serializable, memory is simply allocated for the object, and then values are read from the stream and used to set the object's fields. If we use Serializable, the object's constructor isn't called! All the work happens through reflection (the Reflection API, which we briefly mentioned in the last lesson). With Externalizable, the deserialization mechanism is different. The default constructor is called first. Only after that is the created UserInfo object's readExternal() method called. It is responsible for setting the object's fields. That is why any class implementing the Externalizable interface must have a default constructor. Let's add one to our UserInfo class and rerun the code:
import java.io.*;

public class Main {

   public static void main(String[] args) throws IOException, ClassNotFoundException {

       FileInputStream fileInputStream = new FileInputStream("C:\\Users\\Username\\Desktop\\save.ser");
       ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);


       UserInfo userInfo = (UserInfo) objectInputStream.readObject();
       System.out.println(userInfo);

       objectInputStream.close();
   }
}
Console output: Paul Piper's passport data UserInfo \ firstName = 'Paul', lastName = 'Piper', superSecretInformation = 'Paul Piper's passport data' } Now that's something entirely different! First, the decrypted string with secret information was displayed on the console. Then the object we recovered from the file was displayed as a string! So we've successfully solved all the problems :) The topic of serialization and deserialization seems simple, but, as you can see, the lessons have been long. And there's so much more we haven't covered! There are still many subtleties involved when using each of these interfaces. But to avoid exploding your brain from excessive new information, I'll briefly list a few more important points and give you links to additional reading. So, what else do you need to know? First, during serialization (regardless of whether you're using Serializable or Externalizable), pay attention to static variables. When you use Serializable, these fields aren't serialized at all (and, accordingly, their values don't change, because static fields belong to the class, not the object). But when you use Externalizable, you control the process yourself, so technically you could serialize them. But, we don't recommend it, since doing is likely to create lots of subtle bugs. Second, you should also pay attention to variables with the final modifier. When you use Serializable, they are serialized and deserialized as usual, but when you use Externalizable, it is impossible to deserialize a final variable! The reason is simple: all final fields are initialized when the default constructor is called — after that, their value cannot be changed. Therefore, to serialize objects that have final fields, use the standard serialization provided by Serializable. Third, when you use inheritance, all descendant classes that inherit some Externalizable class must also have default constructors. Here is link to good article about serialization mechanisms: Until next time! :)