Everything You Need to Know About Java Serialization Explained
Here's everything you need to know about serialization in Java.
Join the DZone community and get the full member experience.
Join For FreeIn a previous article, we looked at five different ways to create objects in Java, I have explained how deserializing a serialized object creates a new object, and in this blog, I am going to discuss Serialization and Deserialization in details.
We will use below Employee
class object as an example for the explanation
// If we use Serializable interface, static and transient variables do not get serialize
class Employee implements Serializable {
// This serialVersionUID field is necessary for Serializable as well as Externalizable to provide version control,
// Compiler will provide this field if we do not provide it which might change if we modify the class structure of our class, and we will get InvalidClassException,
// If we provide value to this field and do not change it, serialization-deserialization will not fail if we change our class structure.
private static final long serialVersionUID = 2L;
private final String firstName; // Serialization process do not invoke the constructor but it can assign values to final fields
private transient String middleName; // transient variables will not be serialized, serialised object holds null
private String lastName;
private int age;
private static String department; // static variables will not be serialized, serialised object holds null
public Employee(String firstName, String middleName, String lastName, int age, String department) {
this.firstName = firstName;
this.middleName = middleName;
this.lastName = lastName;
this.age = age;
Employee.department = department;
validateAge();
}
private void validateAge() {
System.out.println("Validating age.");
if (age < 18 || age > 70) {
throw new IllegalArgumentException("Not a valid age to create an employee");
}
}
@Override
public String toString() {
return String.format("Employee {firstName='%s', middleName='%s', lastName='%s', age='%s', department='%s'}", firstName, middleName, lastName, age, department);
}
// Custom serialization logic,
// This will allow us to have additional serialization logic on top of the default one e.g. encrypting object before serialization
private void writeObject(ObjectOutputStream oos) throws IOException {
System.out.println("Custom serialization logic invoked.");
oos.defaultWriteObject(); // Calling the default serialization logic
}
// Custom deserialization logic
// This will allow us to have additional deserialization logic on top of the default one e.g. decrypting object after deserialization
private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
System.out.println("Custom deserialization logic invoked.");
ois.defaultReadObject(); // Calling the default deserialization logic
// Age validation is just an example but there might some scenario where we might need to write some custom deserialization logic
validateAge();
}
}
What Are Serialization and Deserialization?
In Java, we create several objects that live and die accordingly, and every object will certainly die when the JVM dies. But sometimes, we might want to reuse an object between several JVMs or we might want to transfer an object to another machine over the network.
Well, serialization allows us to convert the state of an object into a byte stream, which then can be saved into a file on the local disk or sent over the network to any other machine. And deserialization allows us to reverse the process, which means reconverting the serialized byte stream to an object again.
In simple words, object serialization is the process of saving an object's state to a sequence of bytes and deserialization is the process of reconstructing an object from those bytes. Generally, the complete process is called serialization, but I think it is better to classify both as separate for more clarity:
The serialization process is platform independent, an object serialized on one platform can be deserialized on a different platform.
To serialize and deserialize, our object to a file we need to call ObjectOutputStream.writeObject()
and ObjectInputStream.readObject()
as done in the following code:
public class SerializationExample {
public static void main(String[] args) throws IOException, ClassNotFoundException {
Employee empObj = new Employee("Shanti", "Prasad", "Sharma", 25, "IT");
System.out.println("Object before serialization => " + empObj.toString());
// Serialization
serialize(empObj);
// Deserialization
Employee deserialisedEmpObj = deserialize();
System.out.println("Object after deserialization => " + deserialisedEmpObj.toString());
}
// Serialization code
static void serialize(Employee empObj) throws IOException {
try (FileOutputStream fos = new FileOutputStream("data.obj");
ObjectOutputStream oos = new ObjectOutputStream(fos))
{
oos.writeObject(empObj);
}
}
// Deserialization code
static Employee deserialize() throws IOException, ClassNotFoundException {
try (FileInputStream fis = new FileInputStream("data.obj");
ObjectInputStream ois = new ObjectInputStream(fis))
{
return (Employee) ois.readObject();
}
}
}
Only Classes That Implement Serializable Can Be Serialized
Similar to the Cloneable interface for Java cloning in serialization, we have one marker interface, Serializable, which works like a flag for the JVM. Any class that implements Serializable
interface directly or through its parent can be serialized, and classes that do not implement Serializable
can not be serialized.
Java's default serialization process is fully recursive, so whenever we try to serialize one object, the serialization process try to serialize all the fields (primitive and reference) with our class (except static
and transient
fields).
When a class implements theSerializable
interface, all its sub-classes are serializable as well. But when an object has a reference to another object, these objects must implement theSerializable
interface separately. If our class is having even a single reference to a nonSerializable
class then JVM will throwNotSerializableException
.
Why Is Serializable Not Implemented by Object?
Now, the question arises: If Serialization is very basic functionality and any class that does not implement Serializable
can not be serialized, then why is Serializable not implemented by the Object
itself? In this way, all our objects could be serialized by default.
The Object
class does not implement Serializable
interface because we may not want to serialize all the objects, e.g. serializing a thread does not make any sense because thread running in my JVM would be using my system's memory, persisting it and trying to run it in your JVM would make no sense.
The Transient and Static Fields Do Not Get Serialized
If we want to serialize one object but do not want to serialize specific fields, then we can mark those fields as transient.
All the static fields belong to the class instead of the object, and the serialization process serializes the object so static fields can not be serialized.
- Serialization does not care about access modifiers of the field such as
private
. All non transient and non static fields are considered part of an object's persistent state and are eligible for serialisation.- We can assign values to final fields in conscrutors only and serialization process do not invoke any constructor but still it can assign values to final fields.
What Is serialVersionUID? And Why Should We Declare It?
Suppose we have a class and we have serialized its object to a file on the disk, and due to some new requirements, we added/removed one field from our class. Now, if we try to deserialize the already serialized object, we will get InvalidClassException
; why?
We get it because, by default, the JVM associates a version number to each serializable class to control the class versioning. It is used to verify that the serialized and deserialized objects have the same attributes and thus are compatible with deserialization. The version number is maintained in a field called serialVersionUID
. If a serializable class doesn't declare a serialVersionUID
, the JVM will generate one automatically at run-time.
If we change our class structure, e.g. remove/add fields, that version number also changes, and according to the JVM, our class is not compatible with the class version of the serialized object. That's why we get the exception, but if you really think about it, why should it be thrown just because I added a field? Couldn't the field just be set to its default value and then written out next time?
Yes, it can be done by providing the serialVersionUID
field manually and ensuring it is always the same. It is highly recommended that each serializable class declares its serialVersionUID
as the generated one is compiler dependent and thus may result in unexpected InvalidClassExceptions
.
You can use a utility that comes with the JDK distribution called
serialver
to see what that code would be by default (it is just the hash code of the object by default).
Customizing Serialization and Deserialization With writeObject and readObject Methods
The JVM has full control for serializing the object in the default serialization process, but there are lots of downsides to using the default serialization process, some of which are:
- It can not handle the serialization of fields that are not serializable.
- Deserialization process does not invoke constructors while creating the object so it can not call the initialization logic provided by the constructor.
But we can override this the default serialization behavior inside our Java class and provide some additional logic to enhance the normal process. This can be done by providing two methods, writeObject
and readObject
, inside the class that we want to serialize:
// Custom serialization logic will allow us to have additional serialization logic on top of the default one e.g. encrypting object before serialization
private void writeObject(ObjectOutputStream oos) throws IOException {
// Any Custom logic
oos.defaultWriteObject(); // Calling the default serialization logic
// Any Custom logic
}
// Custom deserialization logic will allow us to have additional deserialization logic on top of the default one e.g. decrypting object after deserialization
private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
// Any Custom logic
ois.defaultReadObject(); // Calling the default deserialization logic
// Any Custom logic
}
Declaring both methods as private is necessary (public methods will not work), so rather than the JVM, nothing else can see them. This also proves that neither method is not inherited nor overridden or overloaded. The JVM automatically checks these methods and calls them during the serialization-deserialization process. The JVM can call these private methods, but other objects can not. Thus, the integrity of the class is maintained and the serialization protocol can continue to work as normal.
Even though those specialized private methods are provided, the object serialization works the same way by calling ObjectOutputStream.writeObject()
or ObjectInputStream.readObject()
.
The call to ObjectOutputStream.writeObject()
or ObjectInputStream.readObject()
kicks off the serialization protocol. First, the object is checked to ensure it implements Serializable
, and then, it is checked to see whether either of those private methods is provided. If they are provided, the stream class is passed as the parameter to these methods, giving the code control over its usage.
We can call ObjectOutputStream.defaultWriteObject()
and ObjectInputStream.defaultReadObject()
from these methods to gain default serialization logic. Those calls do what they sound like — they perform the default writing and reading of the serialized object, which is important because we are not replacing the normal process; we are only adding to it.
Those private methods can be used for any customization you want to make in the serialization process, e.g. encryption can be added to the output and decryption to the input (note that the bytes are written and read in cleartext with no obfuscation at all). They could be used to add extra data to the stream, perhaps a company versioning code, the possibilities are truly limitless.
Stopping Serialization and Deserialization
Suppose we have a class that got the serialization capability from its parent, which means our class extends from another class that implements Serializable
.
It means anybody can serialize and deserialize the object of our class. But what if we do not want our class to be serialized or deserialized? For example, what is our class is a singleton and we want to prevent any new object creation? Remember that the deserialization process creates a new object.
To stop the serialization for our class, we can, once again, use the above private methods to just throw the NotSerializableException
. Any attempt to serialize or deserialize our object will now always result in the exception being thrown. And since those methods are declared as private
, nobody can override your methods and change them.
private void writeObject(ObjectOutputStream oos) throws IOException {
throw new NotSerializableException("Serialization is not supported on this object!");
}
private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
throw new NotSerializableException("Serialization is not supported on this object!");
}
However, this is a violation of the Liskov Substitution Principle. And writeReplace
and readResolve
methods can be used to achieve singleton-like behaviours. These methods are used to allow an object to provide an alternative representation for itself within an ObjectStream
. In simple words, readResolve
can be used to change the data that is deserialized through the readObject
method, and writeReplace
can be used to change the data that is serialized through writeObject
.
Java serialization can also be used to deep clone an object. Java cloning is the most debatable topic in Java community and it surely does have its drawbacks but it is still the most popular and easy way of creating a copy of an object until that object is full filling mandatory conditions of Java cloning. I have covered cloning in details in a 3 article long Java Cloning Series which includes articles like Java Cloning And Types Of Cloning (Shallow And Deep) In Details With Example, Java Cloning - Copy Constructor Versus Cloning, Java Cloning - Even Copy Constructors Are Not Sufficient, go ahead and read them if you want to know more about cloning.
Conclusion
- Serialization is the process of saving an object's state to a sequence of bytes, which then can be stored on a file or sent over the network, and deserialization is the process of reconstructing an object from those bytes.
- Only subclasses of the
Serializable
interface can be serialized. - If our class does not implement
Serializable
interface, or if it is having a reference to a non-Serializable
class, then the JVM will throwNotSerializableException
. - All
transient
andstatic
fields do not get serialized. - The
serialVersionUID
is used to verify that the serialized and deserialized objects have the same attributes and thus are compatible with deserialization. - We should create a
serialVersionUID
field in our class so if we change our class structure (adding/removing fields), the JVM will not throughInvalidClassException
. If we do not provide it, the JVM provides one that might change when our class structure changes. - We can override the default serialization behaviour inside our Java class by providing the implementation of
writeObject
andreadObject
methods. - And we can call
ObjectOutputStream.defaultWriteObject()
andObjectInputStream.defaultReadObject
fromwriteObject
andreadObject
methods to get the default serialization and deserialization logic. - We can throw
NotSerializableException
exception fromwriteObject
andreadObject
, if we do not want our class to be serialized or deserialized.
The Java Serialization process can be further customized and enhanced using the Externalizable
interface, which I have explained in How to Customize Serialization In Java By Using Externalizable Interface.
You can find the complete source code for this article on this GitHub repository, and please feel free to provide your valuable feedback.
Published at DZone with permission of Naresh Joshi, DZone MVB. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments