Java Serialization Magic Methods And Their Uses With Example
In a previous article Everything You Need to Know About Java Serialization, we discussed how serializability of a class is enabled by implementing theSerializable
interface. If our class does not implement Serializable
interface or if it is having a reference to a non Serializable
class then JVM will throw NotSerializableException
.
All subtypes of a serializable class are themselves serializable andExternalizable
interface also extends Serializable. So even if we
customize our serialization process using Externalizable our class is still aSerializable
.
The Serializable
interface is a marker interface which has no methods or fields and it works like a flag for the JVM. The Java serialization process provided by ObjectInputStream
and ObjectOutputStream
classes are fully controlled by the JVM.
But what if we want to add some additional logic to enhance this normal process, for example, we may want to encrypt/decrypt our sensitive information before serializing/deserializing it. Java provides us with some additional methods for this purpose which we are going to discuss in this blog.
writeObject and readObject methods
Serializable classes that want to customize or add some additional logic to enhance the normal serialization/deserialization process should providewriteObject
and readObject
methods with these exact signatures:
private void writeObject(java.io.ObjectOutputStream out) throws IOException
private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException
These methods are already discussed in great details under article Everything You Need to Know About Java Serialization.
readObjectNoData method
As described in Java docs of Serializable
class, if we want to initialize the state of the object for its particular class in the event that the serialization stream does not list the given class as a superclass of the object being deserialized then we should provide writeObject
and readObject
methods with these exact signatures:
private void readObjectNoData() throws ObjectStreamException
This may occur in cases where the receiving party uses a different version of the deserialized instance’s class than the sending party, and the receiver’s version extends classes that are not extended by the sender’s version. This may also occur if the serialization stream has tampered; hence, readObjectNoData is useful for initializing deserialized objects properly despite a “hostile” or incomplete source stream.
Each serializable class may define its own readObjectNoData
method. If a serializable class does not define a readObjectNoData
method, then in the circumstances listed above the fields of the class will be initialized to their default values.
writeReplace and readResolve methods
Serializable classes that need to designate an alternative object to be used when writing an object to the stream should provide this special method with the exact signature:
ANY-ACCESS-MODIFIER Object writeReplace() throws ObjectStreamException
And Serializable classes that need to designate a replacement when an instance of it is read from the stream should provide this special method with the exact signature:
ANY-ACCESS-MODIFIER Object readResolve() throws ObjectStreamException
Basically, the writeReplace
method allows the developer to provide a replacement object that will be serialized instead of the original one. And the readResolve
method is used during deserialization process to replace the de-serialized object by another one of our choices.
One of the main usages of writeReplace and readResolve methods is to implement the singleton design pattern with Serialized classes. We know that the deserialization process creates a new object every time and it can also be used as a method to deeply clone an object, which is not good if we have to make our class singleton.
You can read more about Java cloning and serialization on Java Cloning and
Java Serialization topics.
The method readResolve
is called after readObject
has returned (conversely writeReplace
is called before writeObject
and probably on a different object). The object the method returns replaces this
object returned to the user of ObjectInputStream.readObject
and any further back-references to the object in the stream. We can use the writeReplace method to replace serializing object with null so nothing will be serialized and then use the readResolve method to replace the deserialized object with the singleton instance.
validateObject method
If we want to perform certain validations on some of our fields we can do that by implementing ObjectInputValidation
interface and overridingvalidateObject
method from it.
Method validateObject
will automatically get called when we register this validation by calling ObjectInputStream.registerValidation(this, 0)
from readObject
method. It is very useful to verify the stream has not been tampered with, or that the data makes sense before handing it back to your application.
Below example covers code for all above methods
001 002 003 004 005 006 007 008 009 010 011 012 013 014 015 016 017 018 019 020 021 022 023 024 025 026 027 028 029 030 031 032 033 034 035 036 037 038 039 040 041 042 043 044 045 046 047 048 049 050 051 052 053 054 055 056 057 058 059 060 061 062 063 064 065 066 067 068 069 070 071 072 073 074 075 076 077 078 079 080 081 082 083 084 085 086 087 088 089 090 091 092 093 094 095 096 097 098 099 100 101 102 | public class SerializationMethodsExample { public static void main(String[] args) throws IOException, ClassNotFoundException { Employee emp = new Employee( "Naresh Joshi" , 25 ); System.out.println( "Object before serialization: " + emp.toString()); // Serialization serialize(emp); // Deserialization Employee deserialisedEmp = deserialize(); System.out.println( "Object after deserialization: " + deserialisedEmp.toString()); System.out.println(); // This will print false because both object are separate System.out.println(emp == deserialisedEmp); System.out.println(); // This will print false because both `deserialisedEmp` and `emp` are pointing to same object, // Because we replaced de-serializing object in readResolve method by current instance System.out.println(Objects.equals(emp, deserialisedEmp)); } // 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(); } } } class Employee implements Serializable, ObjectInputValidation { private static final long serialVersionUID = 2L; private String name; private int age; public Employee(String name, int age) { this .name = name; this .age = age; } // With ObjectInputValidation interface we get a validateObject method where we can do our validations. @Override public void validateObject() { System.out.println( "Validating age." ); if (age < 18 || age > 70 ) { throw new IllegalArgumentException( "Not a valid age to create an employee" ); } } // 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 } // Replacing de-serializing object with this, private Object writeReplace() throws ObjectStreamException { System.out.println( "Replacing serialising object by this." ); return this ; } // Custom deserialization logic // This will allow us to have additional deserialization logic on top of the default one e.g. performing validations, decrypting object after deserialization. private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException { System.out.println( "Custom deserialization logic invoked." ); ois.registerValidation( this , 0 ); // Registering validations, So our validateObject method can be called. ois.defaultReadObject(); // Calling the default deserialization logic. } // Replacing de-serializing object with this, // It will will not give us a full proof singleton but it will stop new object creation by deserialization. private Object readResolve() throws ObjectStreamException { System.out.println( "Replacing de-serializing object by this." ); return this ; } @Override public String toString() { return String.format( "Employee {name='%s', age='%s'}" , name, age); } } |
You can find the complete source code for this article on this
Github Repository and please feel free to provide your valuable feedback.
Published on Java Code Geeks with permission by Naresh Joshi, partner at our JCG program. See the original article here: Java Serialization Magic Methods And Their Uses With Example Opinions expressed by Java Code Geeks contributors are their own. |