JavaRush /Java Blog /Random EN /Externalizable Interface in Java

Externalizable Interface in Java

Published in the Random EN group
Hello! Today we will continue our introduction to serializing and deserializing Java objects. In the last lecture, we were introduced to the Serializable marker interface , looked at examples of its use, and also learned how to control the serialization process using the transient keyword . Well, “manage the process,” of course, is a strong word. We have one keyword, one version ID, and that's basically it. The rest of the process is “hardwired” inside Java, and there is no access to it. From a convenience point of view, this is, of course, good. But a programmer in his work should focus not only on his own comfort, right? :) There are other factors to consider. Therefore, Serializable is not the only tool for serialization-deserialization in Java. Today we will get acquainted with the Externalizable interface . But even before we moved on to studying it, you might have a reasonable question: why do we need another tool? SerializableI coped with my job, and the automatic implementation of the entire process cannot but rejoice. The examples we looked at weren't complicated either. So what's the deal? Why another interface for essentially the same task? The fact is that Serializableit has a number of disadvantages. Let's list some of them:
  1. Performance. The interface has Serializablemany advantages, but high performance is clearly not one of them.

Introducing the Externalizable Interface - 2

Firstly , the internal mechanism Serializablegenerates a large amount of service information and various types of temporary data during operation.
Secondly (you don’t have to go into this now and read at your leisure if you’re interested), the work Serializableis based on using the Reflection API. This contraption allows you to do things that would seem impossible in Java: for example, change the values ​​of private fields. JavaRush has an excellent article about the Reflection API , you can read about it here.

  1. Flexibility. We don't control the serialization-deserialization process at all when using the Serializable.

    On the one hand, this is very convenient, because if we don’t really care about performance, the ability to not write code seems convenient. But what if we really need to add some of our own features (an example of one of them will be below) to the serialization logic?

    Essentially, all we have to control the process is a keyword transientto exclude some data, and that's it. Sort of like a “toolkit” :/

  2. Safety. This point partially follows from the previous one.

    We haven’t thought much about this before, but what if some information in your class is not intended for “other people’s ears” (more precisely, eyes)? A simple example is a password or other personal user data, which in the modern world is regulated by a bunch of laws.

    Using Serializable, we actually can’t do anything about it. We serialize everything as is.

    But, in a good way, we must encrypt this kind of data before writing it to a file or transmitting it over the network. But Serializableit doesn’t give this opportunity.

Introducing the Externalizable Interface - 3Well, let's finally see what a class would look like using the Externalizable.
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 SERIAL_VERSION_UID = 1L;

   //...конструктор, геттеры, сеттеры, toString()...

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

   }

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

   }
}
As you can see, we have made significant changes! The main one is obvious: when implementing an interface, Externalizableyou must implement two mandatory methods - writeExternal()and readExternal(). As we said earlier, all responsibility for serialization and deserialization will lie with the programmer. However, now you can solve the problem of lack of control over this process! The entire process is programmed directly by you, which, of course, creates a much more flexible mechanism. In addition, the security problem is also solved. As you can see, we have a field in our class: personal data that cannot be stored unencrypted. Now we can easily write code that meets this constraint. For example, add two simple private methods to our class for encrypting and decrypting secret data. We will write them to a file and read them from the file in encrypted form. And we will write and read the rest of the data as is :) As a result, our class will look 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;

   public UserInfo() {
   }

   public UserInfo(String firstName, String lastName, String superSecretInformation) {
       this.firstName = firstName;
       this.lastName = lastName;
       this.superSecretInformation = superSecretInformation;
   }

   @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 have implemented two methods that use the same ObjectOutput outand as parameters ObjectInputthat we have already encountered in the lecture about Serializable. At the right time, we encrypt or decrypt the necessary data, and in this form we use it to serialize our object. Let's see how this will look 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("Ivan", "Ivanov", "Ivan Ivanov's passport data");

       objectOutputStream.writeObject(userInfo);

       objectOutputStream.close();

   }
}
In the encryptString()and methods decryptString(), we specifically added output to the console to check in what form the secret data will be written and read. The code above outputs the following line to the console: SXZhbiBJdmFub3YncyBwYXNzcG9ydCBkYXRh Encryption succeeded! The full contents of the file look like this: ¬н sr UserInfoГ!}ҐџC‚ћ xpt Ivant Ivanovt $SXZhbiBJdmFub3YncyBwYXNzcG9ydCBkYXRhx Now let's try to use the deserialization logic we wrote.
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, there doesn’t seem to be anything complicated here, it should work! Let's run... Exception in thread "main" java.io.InvalidClassException: UserInfo; no valid constructor Introducing the Externalizable Interface - 4 Oops :( It turned out to be not so simple! The deserialization mechanism threw an exception and required us to create a default constructor. I wonder why? SerializableWe managed without it... :/ Here we come to another important nuance. The difference between Serializableand Externalizablelies not only in “extended” access for the programmer and the ability to more flexibly manage the process, but also in the process itself. First of all, in the deserialization mechanism ... When used, Serializablememory is simply allocated for an object, after which values ​​are read from the stream, which fill all its fields . If we use Serializable, the object constructor is not called! All work is done through reflection (Reflection API, which we briefly mentioned in the last lecture). In the case of , the Externalizabledeserialization mechanism will be different. At the beginning, the default constructor is called. And only then UserInfois called on the created object method readExternal(), which is responsible for filling in the fields of the object. That is why any class that implements the interface Externalizablemust have a default constructor . Let's add it to our class UserInfoand 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: Ivan Ivanov's passport data UserInfo{firstName='Ivan', lastName='Ivanov', superSecretInformation='Ivan Ivanov's passport data'} A completely different matter! First, the decrypted string with secret data was output to the console, and then our object recovered from the file in string format! This is how we successfully resolved all the problems :) The topic of serialization and deserialization seems to be simple, but as you can see, our lectures turned out to be long. And that's not all! There are many more subtleties when using each of these interfaces, but so that now your brain does not explode from the volume of new information, I will briefly list a few more important points and provide links to additional reading. So what else do you need to know? Firstly , when serializing (it doesn’t matter whether you use Serializableor Externalizable), pay attention to the variables static. When used, Serializablethese fields are not serialized at all (and, accordingly, their value does not change, since staticthe fields belong to the class, not the object). But when using it, Externalizableyou control the process yourself, so technically this can be done. But it is not recommended, as this is fraught with subtle errors. Secondly , attention should also be paid to variables with the modifier final. When used, Serializablethey are serialized and deserialized as usual, but when used, it is impossible Externalizableto deserialize finala variable ! The reason is simple: all final-fields are initialized when the default constructor is called, and after that their value cannot be changed. Therefore, to serialize objects containing final-fields, use standard serialization via Serializable. Thirdly , when using inheritance, all inheriting classes descending from some Externalizableclass must also have default constructors. Here are some links to good articles about serialization mechanisms: See you! :)
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION