Serializing Java Objects with Non-Serializable Attributes
There are multiple reasons one might want to use custom serialization instead of relying on Java’s default serialization. One of the most common reasons is for performance improvements, but another reason for writing custom serialization is when the default serialization mechanism is unsupported. Specifically, as will be demonstrated in this post, custom serialization can be used to allow a larger object to be serialized even when attributes of that object are not themselves directly serializable.
The next code listing shows a simple class for serializing a given class to a file of the provided name and for deserializing an object from that same file. I will be using it in this post to demonstrate serialization.
SerializationDemonstrator.java
package dustin.examples.serialization; import static java.lang.System.out; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; /** * Simple serialization/deserialization demonstrator. * * @author Dustin */ public class SerializationDemonstrator { /** * Serialize the provided object to the file of the provided name. * @param objectToSerialize Object that is to be serialized to file; it is * best that this object have an individually overridden toString() * implementation as that is used by this method for writing our status. * @param fileName Name of file to which object is to be serialized. * @throws IllegalArgumentException Thrown if either provided parameter is null. */ public static <T> void serialize(final T objectToSerialize, final String fileName) { if (fileName == null) { throw new IllegalArgumentException( "Name of file to which to serialize object to cannot be null."); } if (objectToSerialize == null) { throw new IllegalArgumentException("Object to be serialized cannot be null."); } try (FileOutputStream fos = new FileOutputStream(fileName); ObjectOutputStream oos = new ObjectOutputStream(fos)) { oos.writeObject(objectToSerialize); out.println("Serialization of Object " + objectToSerialize + " completed."); } catch (IOException ioException) { ioException.printStackTrace(); } } /** * Provides an object deserialized from the file indicated by the provided * file name. * * @param <T> Type of object to be deserialized. * @param fileToDeserialize Name of file from which object is to be deserialized. * @param classBeingDeserialized Class definition of object to be deserialized * from the file of the provided name/path; it is recommended that this * class define its own toString() implementation as that will be used in * this method's status output. * @return Object deserialized from provided filename as an instance of the * provided class; may be null if something goes wrong with deserialization. * @throws IllegalArgumentException Thrown if either provided parameter is null. */ public static <T> T deserialize(final String fileToDeserialize, final Class<T> classBeingDeserialized) { if (fileToDeserialize == null) { throw new IllegalArgumentException("Cannot deserialize from a null filename."); } if (classBeingDeserialized == null) { throw new IllegalArgumentException("Type of class to be deserialized cannot be null."); } T objectOut = null; try (FileInputStream fis = new FileInputStream(fileToDeserialize); ObjectInputStream ois = new ObjectInputStream(fis)) { objectOut = (T) ois.readObject(); out.println("Deserialization of Object " + objectOut + " is completed."); } catch (IOException | ClassNotFoundException exception) { exception.printStackTrace(); } return objectOut; } }
The next code listing illustrates use of the SerializationDemonstrator
class to serialize and deserialize a standard Java string (which is Serializable). A screen snapshot follows the code listing and shows the output (in NetBeans) of running that String through the serialize
and deserialize
methods of the SerializationDemonstrator
class.
Running SerializationDemonstrator Methods on String
SerializationDemonstrator.serialize("Inspired by Actual Events", "string.dat"); final String stringOut = SerializationDemonstrator.deserialize("string.dat", String.class);
The next two code listings are for the class Person.java
and a class that it has as an attribute type (CityState.java
). Although Person
implements Serializable
, the CityAndState
class does not.
Person.java
package dustin.examples.serialization; import java.io.Serializable; /** * Person class. * * @author Dustin */ public class Person implements Serializable { private String lastName; private String firstName; private CityState cityAndState; public Person( final String newLastName, final String newFirstName, final CityState newCityAndState) { this.lastName = newLastName; this.firstName = newFirstName; this.cityAndState = newCityAndState; } public String getFirstName() { return this.firstName; } public String getLastName() { return this.lastName; } @Override public String toString() { return this.firstName + " " + this.lastName + " of " + this.cityAndState; } }
CityAndState.java
package dustin.examples.serialization; /** * Simple class storing city and state names that is NOT Serializable. * * @author Dustin */ public class CityState { private final String cityName; private final String stateName; public CityState(final String newCityName, final String newStateName) { this.cityName = newCityName; this.stateName = newStateName; } public String getCityName() { return this.cityName; } public String getStateName() { return this.stateName; } @Override public String toString() { return this.cityName + ", " + this.stateName; } }
The next code listing demonstrates running SerializationDemonstrator
on the serializable Person
class with a non-serializable CityState
. The code listing is followed by a screen snapshot of the output in NetBeans.
Running SerializationDemonstrator Methods on Serializable Person with Non-Serializable CityState
final Person personIn = new Person("Flintstone", "Fred", new CityState("Bedrock", "Cobblestone")); SerializationDemonstrator.serialize(personIn, "person.dat"); final Person personOut = SerializationDemonstrator.deserialize("person.dat", Person.class);
In this case, the CityState
class is my own class and I could make it Serializable. However, supposing that this class was part of a third-party framework or library and I was not able to change the class itself, I can change Person
to use custom serialization and deserialization and work with CityState
properly. This is shown in the next code listing for the class SerializablePerson
that is adapted from Person
.
SerializablePerson.java
package dustin.examples.serialization; import java.io.IOException; import java.io.InvalidObjectException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.ObjectStreamException; import java.io.Serializable; /** * Person class. * * @author Dustin */ public class SerializablePerson implements Serializable { private String lastName; private String firstName; private CityState cityAndState; public SerializablePerson( final String newLastName, final String newFirstName, final CityState newCityAndState) { this.lastName = newLastName; this.firstName = newFirstName; this.cityAndState = newCityAndState; } public String getFirstName() { return this.firstName; } public String getLastName() { return this.lastName; } @Override public String toString() { return this.firstName + " " + this.lastName + " of " + this.cityAndState; } /** * Serialize this instance. * * @param out Target to which this instance is written. * @throws IOException Thrown if exception occurs during serialization. */ private void writeObject(final ObjectOutputStream out) throws IOException { out.writeUTF(this.lastName); out.writeUTF(this.firstName); out.writeUTF(this.cityAndState.getCityName()); out.writeUTF(this.cityAndState.getStateName()); } /** * Deserialize this instance from input stream. * * @param in Input Stream from which this instance is to be deserialized. * @throws IOException Thrown if error occurs in deserialization. * @throws ClassNotFoundException Thrown if expected class is not found. */ private void readObject(final ObjectInputStream in) throws IOException, ClassNotFoundException { this.lastName = in.readUTF(); this.firstName = in.readUTF(); this.cityAndState = new CityState(in.readUTF(), in.readUTF()); } private void readObjectNoData() throws ObjectStreamException { throw new InvalidObjectException("Stream data required"); } }
The above code listing shows that SerializablePerson
has custom writeObject and readObject methods to support custom serialization/deserialization that handle its attribute of unserializable type CityState
appropriately. A snippet of code for running this class through the SerializationDemonstrator
and the successful output of doing so are shown next.
Running SerializationDemonstrator on SerializablePerson
final SerializablePerson personIn = new SerializablePerson("Flintstone", "Fred", new CityState("Bedrock", "Cobblestone")); SerializationDemonstrator.serialize(personIn, "person.dat"); final SerializablePerson personOut = SerializationDemonstrator.deserialize("person.dat", SerializablePerson.class);
The approach depicted above will allow non-serializable types to be used as attributes of serializable classes without the need to make those fields transient. However, if the CityState
instance shown earlier needs to be used in multiple serializable classes, it might be better to decorate the CityState
class with a serializable decorator and then used that serialized decorator class in classes needing to be serialized. The next code listing shows SerializableCityState
which decorates CityState
with a serialized version.
SerializableCityState
package dustin.examples.serialization; import java.io.IOException; import java.io.InvalidObjectException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.ObjectStreamException; import java.io.Serializable; /** * Simple class storing city and state names that IS Serializable. This class * decorates the non-Serializable CityState class and adds Serializability. * * @author Dustin */ public class SerializableCityState implements Serializable { private CityState cityState; public SerializableCityState(final String newCityName, final String newStateName) { this.cityState = new CityState(newCityName, newStateName); } public String getCityName() { return this.cityState.getCityName(); } public String getStateName() { return this.cityState.getStateName(); } @Override public String toString() { return this.cityState.toString(); } /** * Serialize this instance. * * @param out Target to which this instance is written. * @throws IOException Thrown if exception occurs during serialization. */ private void writeObject(final ObjectOutputStream out) throws IOException { out.writeUTF(this.cityState.getCityName()); out.writeUTF(this.cityState.getStateName()); } /** * Deserialize this instance from input stream. * * @param in Input Stream from which this instance is to be deserialized. * @throws IOException Thrown if error occurs in deserialization. * @throws ClassNotFoundException Thrown if expected class is not found. */ private void readObject(final ObjectInputStream in) throws IOException, ClassNotFoundException { this.cityState = new CityState(in.readUTF(), in.readUTF()); } private void readObjectNoData() throws ObjectStreamException { throw new InvalidObjectException("Stream data required"); } }
This serializable decorator can be used in the Person
class directly and that enclosing Person
can use default serialization because its fields are all serializable. This is shown in the next code listing for Person2
adapted from Person
.
Person2.java
package dustin.examples.serialization; import java.io.Serializable; /** * Person class. * * @author Dustin */ public class Person2 implements Serializable { private final String lastName; private final String firstName; private final SerializableCityState cityAndState; public Person2( final String newLastName, final String newFirstName, final SerializableCityState newCityAndState) { this.lastName = newLastName; this.firstName = newFirstName; this.cityAndState = newCityAndState; } public String getFirstName() { return this.firstName; } public String getLastName() { return this.lastName; } @Override public String toString() { return this.firstName + " " + this.lastName + " of " + this.cityAndState; } }
This code can be executed as shown in the next code listing, which is followed by its output as seen in the NetBeans output window.
Running SerializationDemonstrator Against Person2/SerializableCityState
final Person2 personIn = new Person2("Flintstone", "Fred", new SerializableCityState("Bedrock", "Cobblestone")); SerializationDemonstrator.serialize(personIn, "person.dat"); final Person2 personOut = SerializationDemonstrator.deserialize("person.dat", Person2.class);
Custom serialization can be used to allow a class with attributes of nonserializable types to be serialized without making those attributes of nonserializable type transient. This is a useful technique when serializable classes need to use attributes of types that are not serializable and that cannot be changed.
File Name must be CityState.java not CityAndState.java