Java provides a mechanism, called object serialization where an object can be represented as a sequence of bytes that includes the object’s data as well as information about the object’s type and the types of data stored in the
object.
After a serialized object has been written into a file, it can be read from the file and deserialized that is, the type information and bytes that represent the object and its data can be used to recreate the object in memory.
Basic Serialization in Java
What is Serialization
Serialization is the process of converting an object’s state (including its references) to a sequence of bytes, as well as the process of rebuilding those bytes into a live object at some future time. Serialization is used when you want to persist the object. It is also used by Java RMI to pass objects between JVMs, either as arguments in a method invocation from a client to a server or as return values from a method invocation, or as exceptions thrown by remote methods. In general, serialization is used when we want the object to exist beyond the lifetime of the JVM.
java.io.Serializable is a marker interface (has no body). It is just used to “mark” Java classes as serializable.
Related Article: Object References in Java
The serialization runtime associates with each serializable class a version number, called a serialVersionUID, which is used during de-serialization to verify that the sender and receiver of a serialized object have loaded classes
for that object that are compatible with respect to serialization. If the receiver has loaded a class for the object that has a different serialVersionUID than that of the corresponding sender’s class, then deserialization will result in an InvalidClassException. A serializable class can declare its own serialVersionUID explicitly by declaring a field
named serialVersionUID that must be static, final, and of type long:
ANY-ACCESS-MODIFIER static final long serialVersionUID = 1L;
How to make a class eligible for serialization
To persist an object the respective class must implement the java.io.Serializable interface.
import java.io.Serializable; public class SerialClass implements Serializable { private static final long serialVersionUID = 1L; private Date currentTime; public SerialClass() { currentTime = Calendar.getInstance().getTime(); } public Date getCurrentTime() { return currentTime; } }
How to write an object into a file
Now we need to write this object to a file system. We use java.io.ObjectOutputStream for this purpose.
import java.io.FileOutputStream; import java.io.ObjectOutputStream; import java.io.IOException; public class PersistSerialClass { public static void main(String [] args) { String filename = "time.ser"; SerialClass time = new SerialClass(); //We will write this object to file system. try { ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(filename)); out.writeObject(time); //Write byte stream to file system. out.close(); } catch(IOException ex){ ex.printStackTrace(); } } }
How to recreate an object from its serialized state
The stored object can be read from file system at later time using java.io.ObjectInputStream as shown below:
import java.io.FileInputStream; import java.io.ObjectInputStream; import java.io.IOException; import java.io.java.lang.ClassNotFoundException; public class ReadSerialClass { public static void main(String [] args) { String filename = "time.ser"; SerialClass time = null; try { ObjectInputStream in = new ObjectInputStream(new FileInputStream(filename)); time = (SerialClass)in.readObject(); in.close(); } catch(IOException ex){ ex.printStackTrace(); } catch(ClassNotFoundException cnfe){ cnfe.printStackTrace(); } // print out restored time System.out.println("Restored time: " + time.getTime()); } }
The serialized class is in binary form. The deserialization can be problematic if the class definition changes: see the Versioning of Serialized Objects chapter of the Java Serialization Specification for details.
Serializing an object serializes the entire object graph of which it is the root, and operates correctly in the presence of cyclic graphs. A reset() method is provided to force the ObjectOutputStream to forget about objects that have
already been serialized.
Transient-fields – Serialization
Custom Serialization
In this example we want to create a class that will generate and output to console, a random number between a range of two integers which are passed as arguments during the initialization.
public class SimpleRangeRandom implements Runnable { private int min; private int max; private Thread thread; public SimpleRangeRandom(int min, int max){ this.min = min; this.max = max; thread = new Thread(this); thread.start(); } @Override private void WriteObject(ObjectOutputStreamout) throws IO Exception; private void ReadObject(ObjectInputStream in) throws IOException, ClassNotFoundException; public void run() { while(true) { Random rand = new Random(); System.out.println("Thread: " + thread.getId() + " Random:" + rand.nextInt(max - min)); try { Thread.sleep(10000); } catch (InterruptedException e) { e.printStackTrace(); } } } }
Now if we want to make this class Serializable there will be some problems. The Thread is one of the certain system-level classes that are not Serializable. So we need to declare the thread as transient. By doing this we will be able to serialize the objects of this class but we will still have an issue. As you can see in the constructor we set the min and the max values of our randomizer and after this we start the thread which is responsible for generating and printing the random value. Thus when restoring the persisted object by calling the readObject() the constructor will not run again as there is no creation of a new object. In that case we need to develop a Custom Serialization by providing two methods inside the class. Those methods are:
private void writeObject(ObjectOutputStream out) throws IOException;
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException;
Thus by adding our implementation in the readObject() we can initiate and start our thread:
class RangeRandom implements Serializable, Runnable { private int min; private int max; private transient Thread thread; //transient should be any field that either cannot be serialized e.g Thread or any field you do not want serialized public RangeRandom(int min, int max){ this.min = min; this.max = max; thread = new Thread(this); thread.start(); } @Override public void run() { while(true) { Random rand = new Random(); System.out.println("Thread: " + thread.getId() + " Random:" + rand.nextInt(max - min)); try { Thread.sleep(10000); } catch (InterruptedException e) { e.printStackTrace(); } } } private void writeObject(ObjectOutputStream oos) throws IOException { oos.defaultWriteObject(); } private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { in.defaultReadObject(); thread = new Thread(this); thread.start(); } }
Here is the main for our example:
public class Main { public static void main(String[] args) { System.out.println("Hello"); RangeRandom rangeRandom = new RangeRandom(1,10); FileOutputStream fos = null; ObjectOutputStream out = null; try { fos = new FileOutputStream("test"); out = new ObjectOutputStream(fos); out.writeObject(rangeRandom); out.close(); } catch(IOException ex) { ex.printStackTrace(); } RangeRandom rangeRandom2 = null; FileInputStream fis = null; ObjectInputStream in = null; try { fis = new FileInputStream("test"); in = new ObjectInputStream(fis); rangeRandom2 = (RangeRandom)in.readObject(); in.close(); } catch(IOException ex) { ex.printStackTrace(); } catch(ClassNotFoundException ex) { ex.printStackTrace(); } } }
If you run the main you will see that there are two threads running for each RangeRandom instance and that is because the Thread.start() method is now in both the constructor and the readObject().
Versioning and serialVersionUID
When you implement java.io.Serializable interface to make a class serializable, the compiler looks for a static final field named serialVersionUID of type long. If the class doesn’t have this field declared explicitly then the compiler will create one such field and assign it with a value which comes out of a implementation dependent computation of serialVersionUID. This computation depends upon various aspects of the class and it follows the Object Serialization Specifications given by Sun. But, the value is not guaranteed to be the same across all compiler implementations.
This value is used for checking the compatibility of the classes with respect to serialization and this is done while deserializing
a saved object. The Serialization Runtime verifies that serialVersionUID read from the de-serialized data
and the serialVersionUID declared in the class are exactly the same. If that is not the case, it throws an
InvalidClassException.
It’s highly recommended that you explicitly declare and initialize the static, final field of type long and named ‘serialVersionUID’ in all your classes you want to make Serializable instead of relying on the default computation of
the value for this field even if you are not gonna use versioning. ‘serialVersionUID’ computation is extremely sensitive and may vary from one compiler implementation to another and hence you may turn up getting the InvalidClassException even for the same class just because you used different compiler implementations on the sender and the receiver ends of the serialization process.
public class Example implements Serializable { static final long serialVersionUID = 1L /or some other value/; //… }
As long as serialVersionUID is the same, Java Serialization can handle different versions of a class. Compatible and incompatible changes are;
Compatible Changes
- Adding fields : When the class being reconstituted has a field that does not occur in the stream, that field in the object will be initialized to the default value for its type. If class-specific initialization is needed, the class may provide a readObject method that can initialize the field to nondefault values.
- Adding classes : The stream will contain the type hierarchy of each object in the stream. Comparing this hierarchy in the stream with the current class can detect additional classes. Since there is no information in the stream from which to initialize the object, the class’s fields will be initialized to the default values.
- Removing classes : Comparing the class hierarchy in the stream with that of the current class can detect that a class has been deleted. In this case, the fields and objects corresponding to that class are read from the stream. Primitive fields are discarded, but the objects referenced by the deleted class are created, since they may be referred to later in the stream. They will be garbage-collected when the stream is garbagecollected or reset.
- Adding writeObject/readObject methods : If the version reading the stream has these methods then readObject is expected, as usual, to read the required data written to the stream by the default serialization.
- It should call defaultReadObject first before reading any optional data. The writeObject method is expected as usual to call defaultWriteObject to write the required data and then may write optional data.
- Adding java.io.Serializable : This is equivalent to adding types. There will be no values in the stream for this class so its fields will be initialized to default values. The support for subclassing nonserializable classes requires that the class’s supertype have a no-arg constructor and the class itself will be initialized to default values. If the no-arg constructor is not available, the InvalidClassException is thrown.
- Changing the access to a field : The access modifiers public, package, protected, and private have no effect on the ability of serialization to assign values to the fields.
- Changing a field from static to nonstatic or transient to nontransient : When relying on default serialization to compute the serializable fields, this change is equivalent to adding a field to the class. The new field will be written to the stream but earlier classes will ignore the value since serialization will not assign values to static or transient fields.
Incompatible Changes
- Deleting fields : If a field is deleted in a class, the stream written will not contain its value. When the stream is read by an earlier class, the value of the field will be set to the default value because no value is available in the stream. However, this default value may adversely impair the ability of the earlier version to fulfill its contract.
- Moving classes up or down the hierarchy : This cannot be allowed since the data in the stream appears in the wrong sequence.
- Changing a nonstatic field to static or a nontransient field to transient : When relying on default serialization, this change is equivalent to deleting a field from the class. This version of the class will not write that data to the stream, so it will not be available to be read by earlier versions of the class. As when deleting a field, the field of the earlier version will be initialized to the default value, which can cause the class to fail in unexpected ways.
- Changing the declared type of a primitive field : Each version of the class writes the data with its declared type. Earlier versions of the class attempting to read the field will fail because the type of the data in the stream does not match the type of the field.
- Changing the writeObject or readObject method so that it no longer writes or reads the default field data or changing it so that it attempts to write it or read it when the previous version did not. The default field data must consistently either appear or not appear in the stream.
- Changing a class from Serializable to Externalizable or vice versa is an incompatible change since the stream will contain data that is incompatible with the implementation of the available class.
- Changing a class from a non-enum type to an enum type or vice versa since the stream will contain data that is incompatible with the implementation of the available class.
- Removing either Serializable or Externalizable is an incompatible change since when written it will no longer supply the fields needed by older versions of the class.
- Adding the writeReplace or readResolve method to a class is incompatible if the behavior would produce an object that is incompatible with any older version of the class.
Serialization with Gson
Serialization with Gson is easy and will output correct JSON.
public class Employe { private String firstName; private String lastName; private int age; private BigDecimal salary; private List skills; //getters and setters }
(Serialization)
//Skills List skills = new LinkedList(); skills.add("leadership"); skills.add("Java Experience"); //Employe Employe obj = new Employe(); obj.setFirstName("Christian"); obj.setLastName("Lusardi"); obj.setAge(25); obj.setSalary(new BigDecimal("10000")); obj.setSkills(skills); //Serialization process Gson gson = new Gson(); String json = gson.toJson(obj); //{"firstName":"Christian","lastName":"Lusardi","age":25,"salary":10000,"skills":["leadership","Java Experience"]}
Note that you can not serialize objects with circular references since that will result in infinite recursion.
(Deserialization)
//it's very simple… //Assuming that json is the previous String object…. Employe obj2 = gson.fromJson(json, Employe.class); // obj2 is just like obj
Custom JSON Deserialization with Jackson
We consume rest API as a JSON format and then unmarshal it to a POJO. Jackson’s org.codehaus.jackson.map.ObjectMapper “just works” out of the box and we really don’t do anything in most cases. But sometimes we need custom deserializer to fulfill our custom needs and this tutorial will guide you through the process of creating your own custom deserializer.
Let’s say we have following entities.
public class User { private Long id; private String name; private String email; //getter setter are omitted for clarity }
And
public class Program { private Long id; private String name; private User createdBy; private String contents; //getter setter are omitted for clarity }
Let’s serialize/marshal an object first.
User user = new User(); user.setId(1L); user.setEmail("[email protected]"); user.setName("Bazlur Rahman"); Program program = new Program(); program.setId(1L); program.setName("Program @# 1"); program.setCreatedBy(user); program.setContents("Some contents"); ObjectMapper objectMapper = new ObjectMapper();
final String json = objectMapper.writeValueAsString(program); System.out.println(json);
The above code will produce following JSON-
{
"id": 1,
"name": "Program @# 1",
"createdBy": {
"id": 1,
"name": "Bazlur Rahman",
"email": "[email protected]"
},
"contents": "Some contents"
}
Now can do the opposite very easily. If we have this JSON, we can unmarshal to a program object using ObjectMapper as following –
Now let’s say, this is not the real case, we are going to have a different JSON from an API which doesn’t match with our Program class.
{
"id": 1,
"name": "Program @# 1",
"ownerId": 1
"contents": "Some contents"
}
Look at the JSON string, you can see, it has a different field that is owenerId.
Now if you want to serialize this JSON as we did earlier, you will have exceptions. There are two ways to avoid exceptions and have this serialized –
Ignore the unknown fields
Ignore the onwerId. Add the following annotation in the Program class
@JsonIgnoreProperties(ignoreUnknown = true)
public class Program {}
Write custom deserializer
But there are cases when you actually need this owerId field. Let’s say you want to relate it as an id of the User class.
In such case, you need to write a custom deserializer-
As you can see, first you have to access the JsonNode from the JonsParser. And then you can easily extract information from a JsonNode using the get() method. and you have to make sure about the field name. It should
be the exact name, spelling mistake will cause exceptions.
And finally, you have to register your ProgramDeserializer to the ObjectMapper.
ObjectMapper mapper = new ObjectMapper(); SimpleModule module = new SimpleModule(); module.addDeserializer(Program.class, new ProgramDeserializer()); mapper.registerModule(module); String newJsonString = "{\"id\":1,\"name\":\"Program @# 1\",\"ownerId\":1,\"contents\":\"Some contents\"}"; final Program program2 = mapper.readValue(newJsonString, Program.class);
Alternatively, you can use annotation to register the deserializer directly –
@JsonDeserialize(using = ProgramDeserializer.class)
public class Program {
}