Table of Contents
- Introduction to Serialization
- Basic Serializable Implementation
- serialVersionUID and Versioning
- Transient Fields
- Custom Serialization Methods
- Externalizable Interface
- Inheritance and Serialization
- Security Considerations
- Performance Best Practices
- Real-World Examples
Introduction to Serialization
Serialization is the process of converting an object into a byte stream, and deserialization is the process of reconstructing the object from the byte stream. This enables object persistence, network transmission, and deep copying.
Why Use Serialization?
- Object Persistence: Save objects to files or databases
- Network Communication: Send objects over networks
- Distributed Computing: RMI, EJB, and other distributed technologies
- Caching: Store object state temporarily
- Deep Copying: Create exact copies of objects
Basic Serializable Implementation
Marker Interface Basics
import java.io.*;
// Simple Serializable class
public class Person implements Serializable {
private String name;
private int age;
private String email;
public Person(String name, int age, String email) {
this.name = name;
this.age = age;
this.email = email;
}
// Getters and setters
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public int getAge() { return age; }
public void setAge(int age) { this.age = age; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
@Override
public String toString() {
return "Person{name='" + name + "', age=" + age + ", email='" + email + "'}";
}
}
Serialization and Deserialization Utility
public class SerializationUtils {
// Serialize object to file
public static void serializeObject(Object obj, String filename) throws IOException {
try (FileOutputStream fileOut = new FileOutputStream(filename);
ObjectOutputStream out = new ObjectOutputStream(fileOut)) {
out.writeObject(obj);
System.out.println("Object serialized to: " + filename);
}
}
// Deserialize object from file
public static Object deserializeObject(String filename)
throws IOException, ClassNotFoundException {
try (FileInputStream fileIn = new FileInputStream(filename);
ObjectInputStream in = new ObjectInputStream(fileIn)) {
Object obj = in.readObject();
System.out.println("Object deserialized from: " + filename);
return obj;
}
}
// Serialize to byte array
public static byte[] serializeToBytes(Object obj) throws IOException {
try (ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos)) {
oos.writeObject(obj);
return baos.toByteArray();
}
}
// Deserialize from byte array
public static Object deserializeFromBytes(byte[] data)
throws IOException, ClassNotFoundException {
try (ByteArrayInputStream bais = new ByteArrayInputStream(data);
ObjectInputStream ois = new ObjectInputStream(bais)) {
return ois.readObject();
}
}
}
Basic Usage Example
public class BasicSerializationDemo {
public static void main(String[] args) {
// Create a Person object
Person person = new Person("John Doe", 30, "[email protected]");
// Serialize to file
try {
SerializationUtils.serializeObject(person, "person.ser");
// Deserialize from file
Person deserializedPerson = (Person) SerializationUtils.deserializeObject("person.ser");
System.out.println("Original: " + person);
System.out.println("Deserialized: " + deserializedPerson);
System.out.println("Are they equal? " + person.equals(deserializedPerson));
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
}
}
serialVersionUID and Versioning
Understanding serialVersionUID
The serialVersionUID is a version control mechanism that ensures compatibility during deserialization.
Without serialVersionUID (Automatic Generation)
public class Product implements Serializable {
private String name;
private double price;
public Product(String name, double price) {
this.name = name;
this.price = price;
}
// Java automatically generates serialVersionUID if not provided
// This can cause InvalidClassException if class structure changes
}
With Explicit serialVersionUID
import java.io.Serializable;
public class User implements Serializable {
// Explicit serialVersionUID for version control
private static final long serialVersionUID = 1L;
private String username;
private String password; // Note: In real applications, never serialize passwords!
private String email;
private boolean active;
public User(String username, String password, String email, boolean active) {
this.username = username;
this.password = password;
this.email = email;
this.active = active;
}
// Getters and setters
public String getUsername() { return username; }
public void setUsername(String username) { this.username = username; }
public String getPassword() { return password; }
public void setPassword(String password) { this.password = password; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
public boolean isActive() { return active; }
public void setActive(boolean active) { this.active = active; }
@Override
public String toString() {
return "User{username='" + username + "', email='" + email + "', active=" + active + "}";
}
}
Version Control Example
// Version 1.0 of the class
public class Employee implements Serializable {
private static final long serialVersionUID = 1L;
private String name;
private int employeeId;
private double salary;
public Employee(String name, int employeeId, double salary) {
this.name = name;
this.employeeId = employeeId;
this.salary = salary;
}
// Getters and setters
// ...
}
// Version 2.0 - Added new field with backward compatibility
public class Employee implements Serializable {
private static final long serialVersionUID = 2L; // Updated version
private String name;
private int employeeId;
private double salary;
private String department; // New field added
public Employee(String name, int employeeId, double salary, String department) {
this.name = name;
this.employeeId = employeeId;
this.salary = salary;
this.department = department;
}
// Backward compatible constructor
public Employee(String name, int employeeId, double salary) {
this(name, employeeId, salary, "Unknown");
}
// Getters and setters
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public int getEmployeeId() { return employeeId; }
public void setEmployeeId(int employeeId) { this.employeeId = employeeId; }
public double getSalary() { return salary; }
public void setSalary(double salary) { this.salary = salary; }
public String getDepartment() { return department; }
public void setDepartment(String department) { this.department = department; }
@Override
public String toString() {
return "Employee{name='" + name + "', employeeId=" + employeeId +
", salary=" + salary + ", department='" + department + "'}";
}
}
Version Compatibility Testing
public class VersionCompatibilityTest {
public static void testBackwardCompatibility() {
// Create V1 employee (simulate old version)
Employee v1Employee = new Employee("Alice Smith", 1001, 75000.0);
try {
// Serialize V1 object
SerializationUtils.serializeObject(v1Employee, "employee_v1.ser");
// Modify class to V2 (add department field)
// Now try to deserialize V1 object with V2 class
Employee deserializedEmployee = (Employee) SerializationUtils.deserializeObject("employee_v1.ser");
System.out.println("Deserialized V1 object with V2 class:");
System.out.println("Name: " + deserializedEmployee.getName());
System.out.println("Department: " + deserializedEmployee.getDepartment()); // Will be null
} catch (IOException | ClassNotFoundException e) {
System.err.println("Compatibility error: " + e.getMessage());
}
}
public static void testForwardCompatibility() {
// This will fail if we try to deserialize V2 object with V1 class
Employee v2Employee = new Employee("Bob Johnson", 1002, 80000.0, "Engineering");
try {
SerializationUtils.serializeObject(v2Employee, "employee_v2.ser");
// If we try to deserialize this with V1 class (without department field),
// it will throw InvalidClassException
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
testBackwardCompatibility();
testForwardCompatibility();
}
}
Transient Fields
Using transient Keyword
import java.io.Serializable;
import java.util.Date;
public class BankAccount implements Serializable {
private static final long serialVersionUID = 1L;
private final String accountNumber;
private double balance;
private transient String password; // Won't be serialized
private transient Date lastAccess; // Won't be serialized
private final transient int securityCode = 1234; // Won't be serialized
public BankAccount(String accountNumber, double balance, String password) {
this.accountNumber = accountNumber;
this.balance = balance;
this.password = password;
this.lastAccess = new Date();
}
// Methods that use transient fields
public boolean validatePassword(String inputPassword) {
return this.password != null && this.password.equals(inputPassword);
}
public void updateLastAccess() {
this.lastAccess = new Date();
}
// Custom serialization to handle transient fields
private void writeObject(java.io.ObjectOutputStream out) throws IOException {
out.defaultWriteObject(); // Serialize non-transient fields
// We could encrypt password here before serialization
}
private void readObject(java.io.ObjectInputStream in)
throws IOException, ClassNotFoundException {
in.defaultReadObject(); // Deserialize non-transient fields
// Initialize transient fields after deserialization
this.password = "default"; // Or load from secure storage
this.lastAccess = new Date();
}
// Getters
public String getAccountNumber() { return accountNumber; }
public double getBalance() { return balance; }
public Date getLastAccess() { return lastAccess; }
@Override
public String toString() {
return "BankAccount{accountNumber='" + accountNumber + "', balance=" + balance +
", lastAccess=" + lastAccess + ", passwordSet=" + (password != null) + "}";
}
}
Transient Field Demonstration
public class TransientFieldDemo {
public static void main(String[] args) {
BankAccount account = new BankAccount("123456789", 5000.0, "secret123");
System.out.println("Before serialization:");
System.out.println(account);
System.out.println("Password validated: " + account.validatePassword("secret123"));
try {
// Serialize
SerializationUtils.serializeObject(account, "account.ser");
// Deserialize
BankAccount deserializedAccount = (BankAccount) SerializationUtils.deserializeObject("account.ser");
System.out.println("\nAfter deserialization:");
System.out.println(deserializedAccount);
System.out.println("Password validated: " + deserializedAccount.validatePassword("secret123"));
System.out.println("Password validated with 'default': " +
deserializedAccount.validatePassword("default"));
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
}
}
Sensitive Data Handling
public class SecureUser implements Serializable {
private static final long serialVersionUID = 1L;
private String username;
private transient String password; // Never serialize plain passwords!
private transient String ssn; // Never serialize sensitive data!
private String email;
public SecureUser(String username, String password, String ssn, String email) {
this.username = username;
setPassword(password); // Use setter for potential encryption
this.ssn = ssn;
this.email = email;
}
// Custom serialization - don't serialize sensitive data
private void writeObject(java.io.ObjectOutputStream out) throws IOException {
out.defaultWriteObject();
// Intentionally NOT writing password or SSN
}
private void readObject(java.io.ObjectInputStream in)
throws IOException, ClassNotFoundException {
in.defaultReadObject();
// Sensitive fields remain null after deserialization
this.password = null;
this.ssn = null;
}
// Secure password handling
public void setPassword(String password) {
// In real application, hash the password
this.password = password; // This is just for demonstration
}
public boolean validatePassword(String inputPassword) {
// In real application, compare hashes
return this.password != null && this.password.equals(inputPassword);
}
// Getters (no getter for password!)
public String getUsername() { return username; }
public String getEmail() { return email; }
@Override
public String toString() {
return "SecureUser{username='" + username + "', email='" + email +
"', passwordSet=" + (password != null) + "}";
}
}
Custom Serialization Methods
Implementing writeObject and readObject
import java.io.*;
import java.util.*;
public class CustomSerializationExample implements Serializable {
private static final long serialVersionUID = 1L;
private String name;
private transient List<String> sensitiveData; // Marked transient but we'll handle it
private Date createdDate;
private transient int dataHash; // Calculated field
public CustomSerializationExample(String name, List<String> sensitiveData) {
this.name = name;
this.sensitiveData = new ArrayList<>(sensitiveData);
this.createdDate = new Date();
calculateHash();
}
private void calculateHash() {
this.dataHash = Objects.hash(name, sensitiveData, createdDate);
}
// Custom serialization logic
private void writeObject(ObjectOutputStream out) throws IOException {
out.defaultWriteObject(); // Serialize non-transient fields normally
// Custom serialization for transient fields
out.writeInt(sensitiveData.size());
for (String data : sensitiveData) {
out.writeUTF(encrypt(data)); // Encrypt sensitive data
}
out.writeInt(dataHash); // Save calculated hash
}
// Custom deserialization logic
private void readObject(ObjectInputStream in)
throws IOException, ClassNotFoundException {
in.defaultReadObject(); // Deserialize non-transient fields normally
// Custom deserialization for transient fields
int size = in.readInt();
sensitiveData = new ArrayList<>(size);
for (int i = 0; i < size; i++) {
sensitiveData.add(decrypt(in.readUTF())); // Decrypt sensitive data
}
dataHash = in.readInt();
// Verify data integrity
verifyDataIntegrity();
}
// Simple encryption/decryption for demonstration
private String encrypt(String data) {
// In real application, use proper encryption
return Base64.getEncoder().encodeToString(data.getBytes());
}
private String decrypt(String encrypted) {
// In real application, use proper decryption
return new String(Base64.getDecoder().decode(encrypted));
}
private void verifyDataIntegrity() {
int currentHash = Objects.hash(name, sensitiveData, createdDate);
if (currentHash != dataHash) {
throw new RuntimeException("Data integrity check failed!");
}
}
// Getters
public String getName() { return name; }
public List<String> getSensitiveData() {
return Collections.unmodifiableList(sensitiveData);
}
public Date getCreatedDate() { return createdDate; }
public int getDataHash() { return dataHash; }
@Override
public String toString() {
return "CustomSerializationExample{name='" + name + "', sensitiveData=" +
sensitiveData + ", createdDate=" + createdDate + ", dataHash=" + dataHash + "}";
}
}
Validation During Deserialization
public class ValidatedSerialization implements Serializable {
private static final long serialVersionUID = 1L;
private String id;
private String data;
private transient boolean validated = false;
public ValidatedSerialization(String id, String data) {
this.id = id;
this.data = data;
validate();
}
private void validate() {
if (id == null || id.trim().isEmpty()) {
throw new IllegalArgumentException("ID cannot be null or empty");
}
if (data == null || data.length() > 1000) {
throw new IllegalArgumentException("Data cannot be null or exceed 1000 characters");
}
this.validated = true;
}
private void readObject(ObjectInputStream in)
throws IOException, ClassNotFoundException {
in.defaultReadObject();
// Re-validate after deserialization
try {
validate();
} catch (IllegalArgumentException e) {
throw new InvalidObjectException("Validation failed after deserialization: " + e.getMessage());
}
}
// readObjectNoData for initial versions or special cases
private void readObjectNoData() throws ObjectStreamException {
throw new InvalidObjectException("Cannot deserialize without data");
}
public boolean isValid() {
return validated;
}
@Override
public String toString() {
return "ValidatedSerialization{id='" + id + "', data='" + data + "', validated=" + validated + "}";
}
}
Externalizable Interface
Implementing Externalizable
import java.io.*;
public class ExternalizableExample implements Externalizable {
// Note: serialVersionUID is still recommended
private static final long serialVersionUID = 1L;
private String name;
private int age;
private transient String temporaryData; // Not automatically handled
// Required no-arg constructor for Externalizable
public ExternalizableExample() {
System.out.println("No-arg constructor called");
}
public ExternalizableExample(String name, int age, String temporaryData) {
this.name = name;
this.age = age;
this.temporaryData = temporaryData;
}
@Override
public void writeExternal(ObjectOutput out) throws IOException {
System.out.println("writeExternal called");
// Manual control over what gets written
out.writeUTF(name);
out.writeInt(age);
// We choose not to write temporaryData
}
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
System.out.println("readExternal called");
// Manual control over what gets read
this.name = in.readUTF();
this.age = in.readInt();
// temporaryData remains null/default
this.temporaryData = "Initialized after deserialization";
}
// Getters
public String getName() { return name; }
public int getAge() { return age; }
public String getTemporaryData() { return temporaryData; }
@Override
public String toString() {
return "ExternalizableExample{name='" + name + "', age=" + age +
", temporaryData='" + temporaryData + "'}";
}
}
Externalizable vs Serializable
public class ExternalizableComparison {
public static void comparePerformance() {
ExternalizableExample extObj = new ExternalizableExample("John", 30, "temp");
SerializableExample serObj = new SerializableExample("John", 30, "temp");
try {
// Test serialization size
byte[] extBytes = SerializationUtils.serializeToBytes(extObj);
byte[] serBytes = SerializationUtils.serializeToBytes(serObj);
System.out.println("Externalizable size: " + extBytes.length + " bytes");
System.out.println("Serializable size: " + serBytes.length + " bytes");
System.out.println("Size difference: " + (serBytes.length - extBytes.length) + " bytes");
// Test performance
long startTime = System.nanoTime();
for (int i = 0; i < 1000; i++) {
SerializationUtils.serializeToBytes(extObj);
}
long extTime = System.nanoTime() - startTime;
startTime = System.nanoTime();
for (int i = 0; i < 1000; i++) {
SerializationUtils.serializeToBytes(serObj);
}
long serTime = System.nanoTime() - startTime;
System.out.println("Externalizable time: " + extTime / 1000000 + " ms");
System.out.println("Serializable time: " + serTime / 1000000 + " ms");
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
comparePerformance();
}
}
class SerializableExample implements Serializable {
private static final long serialVersionUID = 1L;
private String name;
private int age;
private transient String temporaryData;
public SerializableExample(String name, int age, String temporaryData) {
this.name = name;
this.age = age;
this.temporaryData = temporaryData;
}
}
Inheritance and Serialization
Serialization in Inheritance Hierarchy
import java.io.*;
// Base class
class Vehicle implements Serializable {
private static final long serialVersionUID = 1L;
private String manufacturer;
private int year;
public Vehicle(String manufacturer, int year) {
this.manufacturer = manufacturer;
this.year = year;
System.out.println("Vehicle constructor");
}
// Getters
public String getManufacturer() { return manufacturer; }
public int getYear() { return year; }
@Override
public String toString() {
return "Vehicle{manufacturer='" + manufacturer + "', year=" + year + "}";
}
}
// Derived class
class Car extends Vehicle {
private static final long serialVersionUID = 1L;
private String model;
private transient int horsePower; // Transient field
public Car(String manufacturer, int year, String model, int horsePower) {
super(manufacturer, year);
this.model = model;
this.horsePower = horsePower;
System.out.println("Car constructor");
}
// Custom serialization for Car
private void writeObject(ObjectOutputStream out) throws IOException {
out.defaultWriteObject(); // Serialize Car's fields
out.writeInt(horsePower); // Manually handle transient field
}
private void readObject(ObjectInputStream in)
throws IOException, ClassNotFoundException {
in.defaultReadObject(); // Deserialize Car's fields
this.horsePower = in.readInt(); // Manually handle transient field
}
// Getters
public String getModel() { return model; }
public int getHorsePower() { return horsePower; }
@Override
public String toString() {
return "Car{" + super.toString() + ", model='" + model + "', horsePower=" + horsePower + "}";
}
}
// Further derived class
class ElectricCar extends Car {
private static final long serialVersionUID = 1L;
private int batteryCapacity;
private transient double chargingTime; // Transient field
public ElectricCar(String manufacturer, int year, String model, int horsePower,
int batteryCapacity, double chargingTime) {
super(manufacturer, year, model, horsePower);
this.batteryCapacity = batteryCapacity;
this.chargingTime = chargingTime;
System.out.println("ElectricCar constructor");
}
// Getters
public int getBatteryCapacity() { return batteryCapacity; }
public double getChargingTime() { return chargingTime; }
@Override
public String toString() {
return "ElectricCar{" + super.toString() + ", batteryCapacity=" + batteryCapacity +
"kWh, chargingTime=" + chargingTime + " hours}";
}
}
Inheritance Serialization Demo
public class InheritanceSerializationDemo {
public static void main(String[] args) {
ElectricCar tesla = new ElectricCar("Tesla", 2023, "Model S", 670, 100, 1.5);
System.out.println("Original: " + tesla);
try {
// Serialize
SerializationUtils.serializeObject(tesla, "electric_car.ser");
// Deserialize
ElectricCar deserializedTesla = (ElectricCar) SerializationUtils.deserializeObject("electric_car.ser");
System.out.println("Deserialized: " + deserializedTesla);
// Verify inheritance hierarchy
System.out.println("\nVerifying inheritance:");
System.out.println("Is Vehicle? " + (deserializedTesla instanceof Vehicle));
System.out.println("Is Car? " + (deserializedTesla instanceof Car));
System.out.println("Is ElectricCar? " + (deserializedTesla instanceof ElectricCar));
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
}
}
Special Cases in Inheritance
// Case 1: Non-serializable parent class
class NonSerializableBase {
private String baseData;
public NonSerializableBase(String baseData) {
this.baseData = baseData;
System.out.println("NonSerializableBase constructor");
}
public String getBaseData() { return baseData; }
// This method will be called during deserialization of child
public NonSerializableBase() {
System.out.println("NonSerializableBase no-arg constructor");
this.baseData = "default";
}
}
class SerializableChild extends NonSerializableBase implements Serializable {
private static final long serialVersionUID = 1L;
private String childData;
public SerializableChild(String baseData, String childData) {
super(baseData);
this.childData = childData;
System.out.println("SerializableChild constructor");
}
public String getChildData() { return childData; }
@Override
public String toString() {
return "SerializableChild{baseData='" + getBaseData() + "', childData='" + childData + "'}";
}
}
Security Considerations
Security Manager and Serialization
public class SecureSerialization implements Serializable {
private static final long serialVersionUID = 1L;
private String data;
public SecureSerialization(String data) {
this.data = data;
}
// Override this method to enforce security checks
private void readObject(ObjectInputStream in)
throws IOException, ClassNotFoundException {
// Check security permissions before deserialization
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
sm.checkPermission(new SerializablePermission("enableSubstitution"));
}
in.defaultReadObject();
// Validate deserialized data
if (data != null && data.contains("malicious")) {
throw new InvalidObjectException("Malicious data detected");
}
}
// Prevent forging of serialized objects
private Object readResolve() throws ObjectStreamException {
// Return a replacement object if necessary
// or validate and return this
return this;
}
// Optional: control replacement during serialization
private Object writeReplace() throws ObjectStreamException {
// Return a replacement object to be serialized instead of this
return this;
}
public String getData() { return data; }
}
Preventing Deserialization Attacks
public class SafeDeserialization {
// Whitelist of allowed classes
private static final Set<String> ALLOWED_CLASSES = Set.of(
"java.lang.String",
"java.util.ArrayList",
"com.yourapp.SafeClass"
);
public static Object safeDeserialize(byte[] data) throws IOException, ClassNotFoundException {
try (ByteArrayInputStream bais = new ByteArrayInputStream(data);
SafeObjectInputStream sois = new SafeObjectInputStream(bais)) {
return sois.readObject();
}
}
// Custom ObjectInputStream with class filtering
static class SafeObjectInputStream extends ObjectInputStream {
public SafeObjectInputStream(InputStream in) throws IOException {
super(in);
}
@Override
protected Class<?> resolveClass(ObjectStreamClass desc)
throws IOException, ClassNotFoundException {
String className = desc.getName();
// Check against whitelist
if (!ALLOWED_CLASSES.contains(className)) {
throw new InvalidClassException("Unauthorized deserialization attempt", className);
}
return super.resolveClass(desc);
}
}
}
Performance Best Practices
Optimizing Serialization Performance
public class HighPerformanceSerialization implements Serializable {
private static final long serialVersionUID = 1L;
// Use primitive types instead of wrappers when possible
private int id; // Instead of Integer
private double value; // Instead of Double
private boolean active; // Instead of Boolean
// Use arrays for large collections of primitives
private transient int[] largeData; // Mark transient and handle manually
// Avoid complex object graphs
private String name; // Simple String instead of complex object
// Use custom serialization for better performance
private void writeObject(ObjectOutputStream out) throws IOException {
// Manual serialization for better control
out.writeInt(id);
out.writeDouble(value);
out.writeBoolean(active);
out.writeUTF(name);
// Handle large array efficiently
if (largeData != null) {
out.writeInt(largeData.length);
for (int i : largeData) {
out.writeInt(i);
}
} else {
out.writeInt(-1);
}
}
private void readObject(ObjectInputStream in)
throws IOException, ClassNotFoundException {
id = in.readInt();
value = in.readDouble();
active = in.readBoolean();
name = in.readUTF();
int length = in.readInt();
if (length >= 0) {
largeData = new int[length];
for (int i = 0; i < length; i++) {
largeData[i] = in.readInt();
}
}
}
// Constructor and methods...
public HighPerformanceSerialization(int id, double value, boolean active, String name, int[] largeData) {
this.id = id;
this.value = value;
this.active = active;
this.name = name;
this.largeData = largeData;
}
}
Serialization Proxy Pattern
public class SerializationProxyPattern implements Serializable {
private static final long serialVersionUID = 1L;
private final String data;
private final int version;
public SerializationProxyPattern(String data, int version) {
this.data = data;
this.version = version;
}
// Serialization proxy - replaces the actual object during serialization
private static class SerializationProxy implements Serializable {
private static final long serialVersionUID = 2L;
private final String data;
private final int version;
SerializationProxy(SerializationProxyPattern pattern) {
this.data = pattern.data;
this.version = pattern.version;
}
// This method is called during deserialization of the proxy
private Object readResolve() {
// Return a new instance of the outer class
return new SerializationProxyPattern(data, version);
}
}
// Replace this object with proxy during serialization
private Object writeReplace() {
return new SerializationProxy(this);
}
// Prevent direct deserialization (will throw exception)
private void readObject(ObjectInputStream in) throws InvalidObjectException {
throw new InvalidObjectException("Proxy required");
}
// Getters
public String getData() { return data; }
public int getVersion() { return version; }
@Override
public String toString() {
return "SerializationProxyPattern{data='" + data + "', version=" + version + "}";
}
}
Real-World Examples
Configuration Object Serialization
import java.io.*;
import java.util.*;
public class ApplicationConfig implements Serializable {
private static final long serialVersionUID = 1L;
private final Properties settings;
private final Date lastModified;
private transient volatile boolean dirty = false;
public ApplicationConfig() {
this.settings = new Properties();
this.lastModified = new Date();
}
public void setProperty(String key, String value) {
settings.setProperty(key, value);
dirty = true;
}
public String getProperty(String key) {
return settings.getProperty(key);
}
public String getProperty(String key, String defaultValue) {
return settings.getProperty(key, defaultValue);
}
public void saveToFile(String filename) throws IOException {
if (dirty) {
SerializationUtils.serializeObject(this, filename);
dirty = false;
}
}
public static ApplicationConfig loadFromFile(String filename)
throws IOException, ClassNotFoundException {
return (ApplicationConfig) SerializationUtils.deserializeObject(filename);
}
// Custom serialization to handle Properties efficiently
private void writeObject(ObjectOutputStream out) throws IOException {
out.defaultWriteObject();
out.writeBoolean(dirty);
}
private void readObject(ObjectInputStream in)
throws IOException, ClassNotFoundException {
in.defaultReadObject();
dirty = in.readBoolean();
}
@Override
public String toString() {
return "ApplicationConfig{settings=" + settings + ", lastModified=" + lastModified +
", dirty=" + dirty + "}";
}
}
Session Object for Web Applications
public class UserSession implements Serializable {
private static final long serialVersionUID = 1L;
private final String sessionId;
private final String userId;
private final Date creationTime;
private Date lastAccessTime;
private final Map<String, Object> attributes;
private transient int accessCount; // Not persisted
public UserSession(String sessionId, String userId) {
this.sessionId = sessionId;
this.userId = userId;
this.creationTime = new Date();
this.lastAccessTime = new Date();
this.attributes = new HashMap<>();
this.accessCount = 0;
}
public void setAttribute(String name, Object value) {
if (value instanceof Serializable) {
attributes.put(name, value);
} else {
throw new IllegalArgumentException("Attribute must be serializable: " + name);
}
}
public Object getAttribute(String name) {
updateAccessTime();
return attributes.get(name);
}
private void updateAccessTime() {
this.lastAccessTime = new Date();
this.accessCount++;
}
// Custom serialization
private void writeObject(ObjectOutputStream out) throws IOException {
out.defaultWriteObject();
// Note: accessCount is transient, so it's not serialized
}
private void readObject(ObjectInputStream in)
throws IOException, ClassNotFoundException {
in.defaultReadObject();
this.accessCount = 0; // Reset access count after deserialization
}
// Getters
public String getSessionId() { return sessionId; }
public String getUserId() { return userId; }
public Date getCreationTime() { return creationTime; }
public Date getLastAccessTime() { return lastAccessTime; }
public int getAccessCount() { return accessCount; }
public boolean isExpired(long timeoutMillis) {
return System.currentTimeMillis() - lastAccessTime.getTime() > timeoutMillis;
}
@Override
public String toString() {
return "UserSession{sessionId='" + sessionId + "', userId='" + userId +
"', creationTime=" + creationTime + ", lastAccessTime=" + lastAccessTime +
", accessCount=" + accessCount + ", attributes=" + attributes.keySet() + "}";
}
}
Distributed Computing Example
// Serializable task for distributed execution
public class DistributedTask implements Serializable, Runnable {
private static final long serialVersionUID = 1L;
private final String taskId;
private final Serializable computation;
private transient volatile Object result;
private transient volatile Exception error;
public DistributedTask(String taskId, Serializable computation) {
this.taskId = taskId;
this.computation = computation;
}
@Override
public void run() {
try {
if (computation instanceof Runnable) {
((Runnable) computation).run();
} else if (computation instanceof Callable) {
this.result = ((Callable<?>) computation).call();
} else {
throw new IllegalArgumentException("Unsupported computation type");
}
} catch (Exception e) {
this.error = e;
}
}
public Object getResult() throws Exception {
if (error != null) {
throw error;
}
return result;
}
public String getTaskId() { return taskId; }
public boolean isCompleted() { return result != null || error != null; }
public boolean hasError() { return error != null; }
@Override
public String toString() {
return "DistributedTask{taskId='" + taskId + "', completed=" + isCompleted() +
", hasError=" + hasError() + "}";
}
}
// Usage in distributed system
public class TaskExecutor {
public byte[] serializeTask(DistributedTask task) throws IOException {
return SerializationUtils.serializeToBytes(task);
}
public DistributedTask deserializeTask(byte[] data)
throws IOException, ClassNotFoundException {
return (DistributedTask) SerializationUtils.deserializeFromBytes(data);
}
public void executeRemoteTask(byte[] taskData) throws Exception {
DistributedTask task = deserializeTask(taskData);
// Execute on remote node
new Thread(task).start();
// Wait for completion (simplified)
while (!task.isCompleted()) {
Thread.sleep(100);
}
if (task.hasError()) {
throw task.error;
}
}
}
Summary
Key Points:
- Serializable is a marker interface - no methods to implement
- Always declare serialVersionUID for version control
- Use transient for sensitive or unnecessary fields
- Implement custom serialization with writeObject/readObject for complex scenarios
- Consider Externalizable for performance-critical applications
- Handle inheritance properly - parent classes should be serializable or have no-arg constructors
- Implement security measures to prevent deserialization attacks
- Use serialization proxies for complex object graphs
When to Use Serialization:
- Object persistence to files or databases
- Network communication in distributed systems
- Session management in web applications
- Caching object states
- Deep copying objects
When to Avoid Serialization:
- Security-sensitive applications without proper safeguards
- Performance-critical systems where speed matters
- When better alternatives exist (JSON, XML, Protocol Buffers)
- For complex object graphs with circular references
Serialization is a powerful feature but should be used judiciously with proper security considerations and performance optimizations.