Introduction
In this article, we'll discuss a concept related to serialization and deserialization in Java. Although sometimes considered as "part of the black magic of the Java serialization API", in this article we'll see that serialVersionUID
is in fact quite straightforward and simple.
First, we'll gloss over serialization and deserialization to recollect some important ideas that we're going to need later on. Afterwards, we'll dive deeper into serialVersionUID
and show what it is and how it works.
Finally, we'll conclude by showing an example which should tie everything together.
Serialization and Deserialization
Serialization is the process of storing the state of an object so that it can be persisted into a database, transferred over the network, written into a file, etc. How exactly does serialization work is beyond the scope of this article, but in general - it works by converting the object into a byte stream which can then be used as any other stream of information, e.g. transferred through a network socket.
Deserialization is the process opposite to serialization. It takes the byte-stream representation of an object (e.g. from a file or a socket) and converts it back into a Java object that lives inside the JVM.
Before either serialization or deserialization can be performed on an object, it is necessary that this object (i.e. its class) implements the Serializable
interface. The Serializable
interface is used to "mark" classes which can be (de)serialized.
Without a class implementing this interface, it's not possible to either serialize or deserialize objects from that class. In the words of the Serializable Javadoc:
"Serializability of a class is enabled by the class implementing the java.io.Serializable interface*.
What is the serialVersionUID?
For serialization and deserialization to work properly, each serializable class must have a version number associated with it - the serialVersionUID. The purpose of this value is to make sure that the classes used both by the sender (the one that serializes) and the receiver (the one that deserializes) of the serialized object are compatible with each other.
If we think about this, it makes much more sense. There should be some mechanism of determining whether the object that was sent matches the object that was received. Otherwise, it could happen, for example, that a change was made to the class of an object before its serialization which the receiver isn't aware of.
Upon reading the object (i.e. deserialization), the reader could load the "new" object into the "old" representation. This could have annoying consequences at best, and complete disarray of the business logic at worst.
That's precisely the reason why the serialVersionUID
exists and is typically used with all serializable objects. It is used to verify that both "versions" of an object (at the sender's and the receiver's side) are compatible, i.e. identical.
In case an update indeed needs to be made to the class, this can be denoted by incrementing the value of the serialVersionUID
. The serialized version will thus have an updated UID which will be stored together with the object and delivered to the reader.
If the reader doesn't have the newest version of the class, an InvalidClassException
will be thrown.
How to Generate serialVersionUID?
As per the documentation, each serialVersionUID field must be static
, final
, and of type long
. The access modifier can be arbitrary, but it is strongly recommended that all declarations use the private
modifier.
In that case, the modifier will only apply to the current class and not its subclasses which is the expected behavior; we don't want a class to be influenced by anything else other than itself. With all that said, this is how a properly constructed serialVersionUID
might look like:
private static final long serialVersionUID = 42L;
Earlier we mentioned that all serializable classes must implement the Serializable
interface.
This interface suggests that all serializable classes can declare a serialVersionUID
, but are not obliged to. In case a class doesn't have an explicitly declared serialVersionUID value, one will be generated by the serialization runtime.
However, it is strongly recommended that all serializable classes explicitly declare a
serialVersionUID
value.
This is because the default, the serialVersionUID
computation is complex, and thus sensitive to very slight differences in environments. If two different compilers are used in the serialization-deserialization process, an InvalidClassException
may be thrown during deserialization because the classes will seemingly not match even though they contain the same contents, verbatim.
Finally, if there are any transient
or static
fields present in the class, they will be ignored during the serialization process and will be null
after deserialization.
serialVersionUID Example
Let's define a class that we'll be using for serialization and deserialization. Of course, it'll implement the Serializable
interface and we'll start off with the serialVersionUID
being 1
:
public class Spaceship implements Serializable {
private static final long serialVersionUID = 1L;
private Pilot pilot;
private Engine engine;
private Hyperdrive hyperdrive;
public void fly() {
System.out.println("We're about to fly high among the stars!");
}
// Constructor, Getters, Setters
}
Next, we'll implement a serializeObject()
method which will be responsible for serializing the object and writing it out to a .ser
file:
public void serializeObject(Spaceship spaceship) {
ObjectOutputStream out = new ObjectOutputStream(
new FileOutputStream("./spaceship.ser")
);
out.writeObject(spaceship);
out.close();
}
Our method serializes the spaceship
object into a .ser
file via a FileOutputStream
. This file now contains the serialized contents of our object.
Now, let's implement a deserializeObject()
method, which takes in that .ser
file and constructs an object back from it:
public void deserializeObject(String filepath) {
Spaceship ship;
ObjectInputStream in = new ObjectInputStream(
new FileInputStream(filepath)
);
ship = (Spaceship) in.readObject();
in.close();
ship.fly();
}
Check out our hands-on, practical guide to learning Git, with best-practices, industry-accepted standards, and included cheat sheet. Stop Googling Git commands and actually learn it!
Let's call these two and observe the output:
public class Main {
public static void main(String[] args) {
Spaceship spaceship = new Spaceship();
serializeObject(spaceship);
deserializeObject("./spaceship.ser");
}
}
This will result in:
We're about to fly high among the stars!
Our deserializeObject()
method loaded the serialized file into the JVM and successfully converted it into a Spaceship
object.
To demonstrate the problem mentioned earlier regarding versioning - let's change the value of the serialVersionUID
from 1L
to 2L
in our Spaceship
class.
Afterwards, let's modify our main()
method to read the file again, without writing it with the modified serialVersionUID
:
public class Main {
public static void main(String[] args) {
deserializeObject("./spaceship.ser");
}
}
Of course, this will result in:
Exception in thread "main" java.io.InvalidClassException ...
As expected, the reason for the exception lies in the serialVersionUID
.
Because we haven't written the new data after updating the serialVersionUID
value to 2L
, the serialized object still contains 1L
as its serialVersionUID
.
However, the deserializeObject()
method was expecting this value to be 2L
because that is the actual new value from within the Spaceship
instance. Due to this inconsistency between the stored and the restored state of the Spaceship
object, the exception was appropriately thrown.
Conclusion
Serialization and deserialization are powerful and common techniques used for storing or transmitting data structures and objects. Sometimes it's easy to overlook certain important details such as the serialVersionUID
, especially given the fact that IDEs typically generate it automatically.
Hopefully, it should be a bit more clear now what its purpose is and how to use it properly in the projects to come.