Serialization in Java

Table of Contents

  1. Introduction to Serialization
  2. Serialization Interface
  3. ObjectOutputStream
  4. ObjectInputStream
  5. transient Keyword
  6. serialVersionUID
  7. Custom Serialization
  8. Inheritance and Serialization
  9. Externalizable Interface
  10. Security Concerns
  11. Best Practices
  12. Examples

Introduction to Serialization

Serialization is the process of converting an object into a byte stream to store the object or transmit it to memory, a database, or a file. Deserialization is the reverse process - reconstructing the object from the byte stream.

Key Concepts:

  • Persistence: Save object state for later use
  • Network Transmission: Send objects over network
  • Deep Copy: Create copies of objects
  • Distributed Computing: Share objects between JVMs

Serialization Interface

The java.io.Serializable interface is a marker interface (no methods) that indicates a class can be serialized.

import java.io.Serializable;
public class Person implements Serializable {
private String name;
private int age;
private String email;
// Constructors, getters, setters
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 + "'}";
}
}

ObjectOutputStream

ObjectOutputStream is used to serialize objects to an output stream.

Key Methods:

  • writeObject(Object obj) - Serializes an object
  • flush() - Flushes the stream
  • close() - Closes the stream
import java.io.*;
public class SerializationDemo {
public static void serializeObject(Object obj, String filename) {
try (FileOutputStream fileOut = new FileOutputStream(filename);
ObjectOutputStream out = new ObjectOutputStream(fileOut)) {
out.writeObject(obj);
System.out.println("Object serialized successfully: " + filename);
} catch (IOException e) {
System.err.println("Serialization error: " + e.getMessage());
}
}
}

ObjectInputStream

ObjectInputStream is used to deserialize objects from an input stream.

Key Methods:

  • readObject() - Deserializes an object
  • close() - Closes the stream
public class DeserializationDemo {
public static Object deserializeObject(String filename) {
try (FileInputStream fileIn = new FileInputStream(filename);
ObjectInputStream in = new ObjectInputStream(fileIn)) {
Object obj = in.readObject();
System.out.println("Object deserialized successfully: " + filename);
return obj;
} catch (IOException | ClassNotFoundException e) {
System.err.println("Deserialization error: " + e.getMessage());
return null;
}
}
}

transient Keyword

The transient keyword prevents fields from being serialized.

import java.io.Serializable;
public class User implements Serializable {
private String username;
private transient String password; // Won't be serialized
private transient Session currentSession; // Transient reference
public User(String username, String password) {
this.username = username;
this.password = password;
this.currentSession = new Session(); // This won't be serialized
}
// Getters and setters
public String getUsername() { return username; }
public String getPassword() { return password; } // Will be null after deserialization
@Override
public String toString() {
return "User{username='" + username + "', password='" + 
(password != null ? "***" : "null") + "'}";
}
}
class Session {
// This class doesn't implement Serializable
}

serialVersionUID

The serialVersionUID is a version control mechanism for serialization.

import java.io.Serializable;
public class Employee implements Serializable {
// Explicitly declare serialVersionUID for version control
private static final long serialVersionUID = 1L;
private String name;
private int employeeId;
private String department;
// Version 1: Only basic fields
public Employee(String name, int employeeId, String department) {
this.name = name;
this.employeeId = employeeId;
this.department = department;
}
// Getters and setters
public String getName() { return name; }
public int getEmployeeId() { return employeeId; }
public String getDepartment() { return department; }
@Override
public String toString() {
return "Employee{name='" + name + "', id=" + employeeId + 
", department='" + department + "'}";
}
}
// Updated version of the class
class EmployeeV2 implements Serializable {
private static final long serialVersionUID = 2L; // Updated version
private String name;
private int employeeId;
private String department;
private double salary; // New field added
public EmployeeV2(String name, int employeeId, String department, double salary) {
this.name = name;
this.employeeId = employeeId;
this.department = department;
this.salary = salary;
}
// Getters and setters
public double getSalary() { return salary; }
public void setSalary(double salary) { this.salary = salary; }
@Override
public String toString() {
return "EmployeeV2{name='" + name + "', id=" + employeeId + 
", department='" + department + "', salary=" + salary + "}";
}
}

Custom Serialization

You can customize serialization by implementing writeObject() and readObject() methods.

import java.io.*;
public class BankAccount implements Serializable {
private static final long serialVersionUID = 1L;
private String accountNumber;
private transient String accountHolder; // Custom serialization
private double balance;
private transient String sensitiveData;
public BankAccount(String accountNumber, String accountHolder, double balance) {
this.accountNumber = accountNumber;
this.accountHolder = accountHolder;
this.balance = balance;
this.sensitiveData = "Confidential: " + accountNumber;
}
// Custom serialization logic
private void writeObject(ObjectOutputStream out) throws IOException {
out.defaultWriteObject(); // Serialize non-transient fields
// Custom serialization for transient fields
out.writeObject(accountHolder != null ? accountHolder : "Unknown");
// Encrypt sensitive data before serialization
String encrypted = encrypt(sensitiveData);
out.writeObject(encrypted);
}
// Custom deserialization logic
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject(); // Deserialize non-transient fields
// Custom deserialization for transient fields
accountHolder = (String) in.readObject();
// Decrypt sensitive data after deserialization
String encrypted = (String) in.readObject();
sensitiveData = decrypt(encrypted);
}
private String encrypt(String data) {
// Simple encryption for demonstration
return new StringBuilder(data).reverse().toString();
}
private String decrypt(String data) {
// Simple decryption for demonstration
return new StringBuilder(data).reverse().toString();
}
// Getters and setters
public String getAccountNumber() { return accountNumber; }
public String getAccountHolder() { return accountHolder; }
public double getBalance() { return balance; }
public String getSensitiveData() { return sensitiveData; }
@Override
public String toString() {
return "BankAccount{accountNumber='" + accountNumber + 
"', accountHolder='" + accountHolder + 
"', balance=" + balance + "}";
}
}

Inheritance and Serialization

Serialization with Inheritance

import java.io.*;
class Person implements Serializable {
private static final long serialVersionUID = 1L;
protected String name;
protected int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public Person() {
// No-arg constructor for deserialization
}
@Override
public String toString() {
return "Person{name='" + name + "', age=" + age + "}";
}
}
class Student extends Person {
private static final long serialVersionUID = 1L;
private String studentId;
private transient double gpa; // Transient field
public Student(String name, int age, String studentId, double gpa) {
super(name, age);
this.studentId = studentId;
this.gpa = gpa;
}
// Custom serialization for Student
private void writeObject(ObjectOutputStream out) throws IOException {
out.defaultWriteObject(); // Serialize Student fields
// Parent class fields are automatically serialized
}
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject(); // Deserialize Student fields
// Parent class fields are automatically deserialized
}
@Override
public String toString() {
return "Student{name='" + name + "', age=" + age + 
", studentId='" + studentId + "', gpa=" + gpa + "}";
}
}
class Professor extends Person {
private static final long serialVersionUID = 1L;
private String department;
private double salary;
public Professor(String name, int age, String department, double salary) {
super(name, age);
this.department = department;
this.salary = salary;
}
@Override
public String toString() {
return "Professor{name='" + name + "', age=" + age + 
", department='" + department + "', salary=" + salary + "}";
}
}

Externalizable Interface

The Externalizable interface provides complete control over serialization.

import java.io.*;
public class Product implements Externalizable {
private String name;
private double price;
private int quantity;
private transient String internalCode; // Won't be serialized by default
// Required public no-arg constructor for Externalizable
public Product() {
System.out.println("No-arg constructor called");
}
public Product(String name, double price, int quantity) {
this.name = name;
this.price = price;
this.quantity = quantity;
this.internalCode = "CODE-" + name.toUpperCase();
}
@Override
public void writeExternal(ObjectOutput out) throws IOException {
// Complete control over what gets written
out.writeUTF(name);
out.writeDouble(price);
out.writeInt(quantity);
// We can choose to serialize transient fields
out.writeUTF(internalCode != null ? internalCode : "UNKNOWN");
}
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
// Complete control over what gets read
this.name = in.readUTF();
this.price = in.readDouble();
this.quantity = in.readInt();
this.internalCode = in.readUTF();
}
// Getters and setters
public String getName() { return name; }
public double getPrice() { return price; }
public int getQuantity() { return quantity; }
public String getInternalCode() { return internalCode; }
@Override
public String toString() {
return "Product{name='" + name + "', price=" + price + 
", quantity=" + quantity + ", internalCode='" + internalCode + "'}";
}
}

Security Concerns

Security Best Practices

import java.io.*;
import java.security.*;
public class SecureSerialization {
// Example of validating objects during deserialization
public static class SecureObject implements Serializable {
private static final long serialVersionUID = 1L;
private String data;
public SecureObject(String data) {
this.data = data;
}
// Validate object state after deserialization
private void readObject(ObjectInputStream in) 
throws IOException, ClassNotFoundException {
in.defaultReadObject();
validateObject();
}
private void validateObject() {
if (data == null || data.trim().isEmpty()) {
throw new SecurityException("Invalid object state: data cannot be null or empty");
}
// Add more validation as needed
}
public String getData() { return data; }
}
// Using object filtering (Java 9+)
public static class ObjectInputFilterDemo {
public static Object deserializeWithFilter(String filename) throws IOException, ClassNotFoundException {
try (FileInputStream fileIn = new FileInputStream(filename);
ObjectInputStream in = new ObjectInputStream(fileIn)) {
// Set filter to allow only specific classes
in.setObjectInputFilter(filterInfo -> {
Class<?> clazz = filterInfo.serialClass();
if (clazz != null) {
if (clazz.getName().startsWith("java.") || 
clazz.getName().startsWith("javax.")) {
return ObjectInputFilter.Status.REJECTED;
}
if (clazz == Person.class || clazz == Employee.class) {
return ObjectInputFilter.Status.ALLOWED;
}
}
return ObjectInputFilter.Status.REJECTED;
});
return in.readObject();
}
}
}
}

Best Practices

1. Always Declare serialVersionUID

// ✅ Good practice
public class MyClass implements Serializable {
private static final long serialVersionUID = 1L;
// class body
}
// ❌ Bad practice - auto-generated UID can cause issues
public class MyClass implements Serializable {
// No explicit serialVersionUID
}

2. Use transient for Sensitive Data

public class UserCredentials implements Serializable {
private String username;
private transient String password; // Won't be serialized
private transient String securityToken;
// Sensitive data should be transient
}

3. Implement Custom Serialization When Needed

public class ComplexObject implements Serializable {
private transient SomeNonSerializableComponent component;
private void writeObject(ObjectOutputStream out) throws IOException {
out.defaultWriteObject();
// Custom serialization logic
}
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject();
// Custom deserialization logic
initializeTransientFields();
}
private void initializeTransientFields() {
// Reinitialize transient fields
this.component = new SomeNonSerializableComponent();
}
}

4. Handle Versioning Carefully

public class VersionedClass implements Serializable {
private static final long serialVersionUID = 2L; // Update when class changes
// Old fields
private String oldField;
// New fields should handle backward compatibility
private String newField;
private void readObject(ObjectInputStream in) 
throws IOException, ClassNotFoundException {
in.defaultReadObject();
// Handle missing fields in older versions
if (newField == null) {
newField = "default-value";
}
}
}

Examples

Complete Working Example

import java.io.*;
import java.util.*;
public class SerializationCompleteExample {
public static void main(String[] args) {
// Example 1: Basic Serialization
basicSerializationExample();
// Example 2: Collection Serialization
collectionSerializationExample();
// Example 3: Custom Serialization
customSerializationExample();
// Example 4: Inheritance Serialization
inheritanceSerializationExample();
}
public static void basicSerializationExample() {
System.out.println("=== Basic Serialization Example ===");
Person person = new Person("John Doe", 30, "[email protected]");
// Serialize
String filename = "person.ser";
try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(filename))) {
out.writeObject(person);
System.out.println("Serialized: " + person);
} catch (IOException e) {
e.printStackTrace();
}
// Deserialize
try (ObjectInputStream in = new ObjectInputStream(new FileInputStream(filename))) {
Person deserializedPerson = (Person) in.readObject();
System.out.println("Deserialized: " + deserializedPerson);
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
}
public static void collectionSerializationExample() {
System.out.println("\n=== Collection Serialization Example ===");
List<Person> people = Arrays.asList(
new Person("Alice", 25, "[email protected]"),
new Person("Bob", 35, "[email protected]"),
new Person("Charlie", 28, "[email protected]")
);
// Serialize collection
String filename = "people.ser";
try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(filename))) {
out.writeObject(people);
System.out.println("Serialized " + people.size() + " people");
} catch (IOException e) {
e.printStackTrace();
}
// Deserialize collection
try (ObjectInputStream in = new ObjectInputStream(new FileInputStream(filename))) {
@SuppressWarnings("unchecked")
List<Person> deserializedPeople = (List<Person>) in.readObject();
System.out.println("Deserialized " + deserializedPeople.size() + " people:");
deserializedPeople.forEach(System.out::println);
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
}
public static void customSerializationExample() {
System.out.println("\n=== Custom Serialization Example ===");
BankAccount account = new BankAccount("123456789", "John Smith", 1500.75);
// Serialize with custom logic
String filename = "account.ser";
try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(filename))) {
out.writeObject(account);
System.out.println("Serialized: " + account);
} catch (IOException e) {
e.printStackTrace();
}
// Deserialize with custom logic
try (ObjectInputStream in = new ObjectInputStream(new FileInputStream(filename))) {
BankAccount deserializedAccount = (BankAccount) in.readObject();
System.out.println("Deserialized: " + deserializedAccount);
System.out.println("Sensitive data: " + deserializedAccount.getSensitiveData());
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
}
public static void inheritanceSerializationExample() {
System.out.println("\n=== Inheritance Serialization Example ===");
Student student = new Student("Jane Doe", 20, "S12345", 3.8);
Professor professor = new Professor("Dr. Smith", 45, "Computer Science", 75000);
// Serialize both
String studentFile = "student.ser";
String professorFile = "professor.ser";
try (ObjectOutputStream out1 = new ObjectOutputStream(new FileOutputStream(studentFile));
ObjectOutputStream out2 = new ObjectOutputStream(new FileOutputStream(professorFile))) {
out1.writeObject(student);
out2.writeObject(professor);
System.out.println("Serialized student and professor");
} catch (IOException e) {
e.printStackTrace();
}
// Deserialize both
try (ObjectInputStream in1 = new ObjectInputStream(new FileInputStream(studentFile));
ObjectInputStream in2 = new ObjectInputStream(new FileInputStream(professorFile))) {
Student deserializedStudent = (Student) in1.readObject();
Professor deserializedProfessor = (Professor) in2.readObject();
System.out.println("Deserialized Student: " + deserializedStudent);
System.out.println("Deserialized Professor: " + deserializedProfessor);
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
}
}
// Utility class for serialization operations
class SerializationUtils {
public static void serializeToFile(Object obj, String filename) throws IOException {
try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(filename))) {
out.writeObject(obj);
}
}
public static Object deserializeFromFile(String filename) 
throws IOException, ClassNotFoundException {
try (ObjectInputStream in = new ObjectInputStream(new FileInputStream(filename))) {
return in.readObject();
}
}
public static byte[] serializeToBytes(Object obj) throws IOException {
try (ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos)) {
oos.writeObject(obj);
return baos.toByteArray();
}
}
public static Object deserializeFromBytes(byte[] data) 
throws IOException, ClassNotFoundException {
try (ByteArrayInputStream bais = new ByteArrayInputStream(data);
ObjectInputStream ois = new ObjectInputStream(bais)) {
return ois.readObject();
}
}
}

Output:

=== Basic Serialization Example ===
Serialized: Person{name='John Doe', age=30, email='[email protected]'}
Deserialized: Person{name='John Doe', age=30, email='[email protected]'}
=== Collection Serialization Example ===
Serialized 3 people
Deserialized 3 people:
Person{name='Alice', age=25, email='[email protected]'}
Person{name='Bob', age=35, email='[email protected]'}
Person{name='Charlie', age=28, email='[email protected]'}
=== Custom Serialization Example ===
Serialized: BankAccount{accountNumber='123456789', accountHolder='John Smith', balance=1500.75}
Deserialized: BankAccount{accountNumber='123456789', accountHolder='John Smith', balance=1500.75}
Sensitive data: Confidential: 123456789
=== Inheritance Serialization Example ===
Serialized student and professor
Deserialized Student: Student{name='Jane Doe', age=20, studentId='S12345', gpa=0.0}
Deserialized Professor: Professor{name='Dr. Smith', age=45, department='Computer Science', salary=75000.0}

Important Notes

  1. Static fields are not serialized
  2. Transient fields are not serialized
  3. Parent class fields are serialized if parent implements Serializable
  4. Object graphs are serialized (references are followed)
  5. Circular references can cause issues
  6. Security: Be careful with deserializing untrusted data
  7. Performance: Serialization can be slow for large objects
  8. Versioning: Always consider backward compatibility

Serialization is a powerful feature in Java but should be used carefully considering security, performance, and maintainability aspects.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper