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:
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.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 :/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.
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 SERIAL_VERSION_UID = 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;
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 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! :)
GO TO FULL VERSION