CACS204 Object Oriented Programming in Java

Object Oriented Programming in JavaUnit 1510 min read

Serialization in Java: Objects to Bytes & Back

Unit 15 of Object Oriented Programming in Java covers serialization—how to convert Java objects into byte streams for storage/transmission and reconstruct them, including transient fields, versioning, and security risks. Learn syntax, use cases, and pitfalls with real-world examples from eSewa and banking systems.

TAKEAWAYS:

  • Serialization converts objects into byte streams using Serializable and ObjectOutputStream/ObjectInputStream.
  • Transient fields are excluded from serialization; static fields are ignored.
  • Versioning via serialVersionUID ensures compatibility across updates.
  • Security risks include malicious object deserialization (e.g., attacks via crafted byte streams).
  • Used in distributed systems (e.g., RMI, Hibernate) and persistence (saving game states, user profiles).

1. What is Serialization?

Serialization is the process of converting an object’s state into a byte stream to store it or transmit it over a network. When needed, the byte stream is deserialized back into an object.

Why Use Serialization?

  • Persistence: Save object state to files/databases (e.g., user profiles in eSewa).
  • Network Transmission: Send objects between systems (e.g., Daraz order processing).
  • Distributed Computing: Share objects in RMI or Spring frameworks.

Key Interfaces/Classes

Interface/Class Purpose
Serializable Marker interface (no methods); marks a class as serializable.
ObjectOutputStream Writes objects to a stream (e.g., file/network).
ObjectInputStream Reads objects from a stream.
Externalizable Custom serialization logic (rarely used).

2. How Serialization Works

Step-by-Step Process

  1. Mark the class as Serializable:
    class User implements Serializable {
        private String name;
        private transient String password; // Not serialized
        private static int count;          // Ignored
    }
    
  2. Serialize to a file:
    try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("user.dat"))) {
        User user = new User("Alice", "pass123");
        oos.writeObject(user); // Converts User to bytes
    }
    
  3. Deserialize from the file:
    try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("user.dat"))) {
        User loadedUser = (User) ois.readObject(); // Reconstructs User
    }
    

Visual: Serialization Flow

flowchart TD
    A["Object (User)"]
    B["ObjectOutputStream"]
    C["Byte Stream (user.dat)"]
    D["ObjectInputStream"]
    E["Reconstructed Object"]
    A -->|"writeObject()"| B -->|"File"| C -->|"readObject()"| D --> E

What Gets Serialized?

  • Included: Instance variables (non-static, non-transient).
  • Excluded:
    • Static fields (belong to the class, not objects).
    • Transient fields (marked with transient).
    • Non-serializable parent classes (unless they implement Serializable).

3. Real-World Examples

In the Real World

  1. eSewa (Nepal):

    • Uses serialization to save user transaction history (e.g., electricity bill payments) in a portable format for backup/recovery.
    • How: Objects like TransactionRecord implement Serializable and are stored in encrypted byte streams.
  2. Khalti (Digital Wallet):

    • Serializes payment request objects when sending them to banks for processing.
    • How: A PaymentRequest object is converted to bytes, transmitted securely, and deserialized by the bank’s system.
  3. Nepal Rastra Bank’s Loan Systems:

    • Serializes loan applicant data (e.g., LoanApplication) to disk for auditing.
    • Why: Ensures data integrity even if the system crashes.

4. Versioning and Compatibility

Problem: Class Changes Over Time

If you modify a class (e.g., add/remove fields), deserialization may fail. Solution: serialVersionUID.

Example: Adding a Field

class User implements Serializable {
    private static final long serialVersionUID = 1L; // Explicit UID
    private String name;
    private String email; // Added later
}
  • Without serialVersionUID: Java generates a default UID. If the class changes, the UID changes → deserialization fails.
  • With serialVersionUID: Ensures backward compatibility.

Visual: Versioning Impact

flowchart TD
    A["Class V1\n(serialVersionUID=1)"] -->|"Adds 'email' field"| B["Class V2\n(serialVersionUID=1)"]
    B -->|"Deserialize old data"| C["Works if UID matches"]
    B -->|"Deserialize old data (UID mismatch)"| D["Error: InvalidClassException"]

5. Security Risks: Deserialization Attacks

Malicious byte streams can exploit vulnerabilities in deserialization. Example:

  • Gadget Chains: Attackers craft byte streams that execute arbitrary code during deserialization.
  • Real-World Impact: Used in ransomware (e.g., Java-based attacks on corporate systems).

Mitigation Strategies

  1. Validate Input: Never deserialize untrusted data.
  2. Use ObjectInputFilter (Java 9+):
    ObjectInputFilter filter = ObjectInputFilter.Config.createFilter("!com.evil.*");
    ois.setObjectInputFilter(filter);
    
  3. Avoid Serialization for Sensitive Data: Use encryption instead.

6. Worked Example: Saving a Bank Account

Scenario

A bank saves Account objects to a file for daily backups. The Account class has:

  • accountNumber (serialized)
  • balance (serialized)
  • password (transient, not saved)

Code Implementation

import java.io.*;

class Account implements Serializable {
    private String accountNumber;
    private transient String password; // Not saved
    private double balance;

    public Account(String accNum, String pwd, double bal) {
        this.accountNumber = accNum;
        this.password = pwd;
        this.balance = bal;
    }
}

public class BankBackup {
    public static void main(String[] args) {
        Account account = new Account("12345", "secret", 1000.0);

        // Serialize
        try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("account.dat"))) {
            oos.writeObject(account);
            System.out.println("Account saved!");
        } catch (IOException e) {
            e.printStackTrace();
        }

        // Deserialize
        try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("account.dat"))) {
            Account loadedAccount = (Account) ois.readObject();
            System.out.println("Account Number: " + loadedAccount.accountNumber);
            System.out.println("Balance: " + loadedAccount.balance);
            // Password is null (transient)
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

Trace Table: Serialization Steps

Step Action State After Step
1 Create Account object accountNumber="12345", balance=1000
2 Call oos.writeObject(account) Byte stream written to account.dat
3 Read from account.dat loadedAccount.accountNumber="12345"
4 Access password null (transient field)

7. Custom Serialization with writeObject/readObject

For full control, override:

private void writeObject(ObjectOutputStream oos) throws IOException {
    oos.writeUTF(accountNumber);
    oos.writeDouble(balance);
    // Skip password
}

private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
    accountNumber = ois.readUTF();
    balance = ois.readDouble();
    password = "default"; // Set default
}

8. Comparison: Serialization vs. Alternatives

Feature Serialization JSON/XML Database Storage
Complexity High (tied to Java) Low (human-readable) Medium (SQL/NoSQL queries)
Performance Fast for Java objects Slower (parsing overhead) Fast (optimized for storage)
Portability Java-only Cross-language Database-specific
Security Vulnerable to attacks Safe if validated Safe (with proper access)
Use Case Internal Java systems APIs, configs Large-scale data storage

9. Common Pitfalls

  1. Forgetting Serializable:
    // Throws NotSerializableException
    oos.writeObject(new NonSerializableClass());
    
  2. Transient Fields:
    • Always mark sensitive fields (e.g., passwords) as transient.
  3. Circular References:
    • Objects referencing each other can cause infinite loops. Use ObjectOutputStream.putFields() to handle this.
  4. Version Mismatch:
    • Always define serialVersionUID for long-lived classes.

Exam Tip

  1. Key Questions:

    • Explain how to serialize an object to a file (4 marks).
    • Differentiate transient and static in serialization (2 marks).
    • Write a program to save/load an object (6 marks).
    • Discuss security risks of deserialization (3 marks).
  2. Common Mistakes to Avoid:

    • Forgetting to close streams (try-with-resources is your friend).
    • Not handling ClassNotFoundException or IOException.
    • Assuming all fields are serialized (check for transient/static).
  3. Quick Code Template:

    // Serialize
    try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("file.dat"))) {
        oos.writeObject(object);
    }
    
    // Deserialize
    try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("file.dat"))) {
        Object obj = ois.readObject();
    }
    

10. Practice Questions

  1. Short Answer:

    • Why is serialVersionUID important?
    • What happens if a parent class is not Serializable?
  2. Programming:

    • Write a program to serialize a Student class with name (serialized) and rollNo (transient). Deserialize and print the name.
  3. Scenario-Based:

    • A bank wants to save LoanApplication objects. The LoanApplication has applicantName, loanAmount, and ssn (transient). Write the serialization code and explain why ssn is marked transient.

Visual Summary: Serialization Process

sequenceDiagram
    participant Object as User Object
    participant OOS as ObjectOutputStream
    participant File as user.dat
    participant OIS as ObjectInputStream
    participant Reconstructed as User Object

    Object->>OOS: writeObject()
    OOS->>File: Writes bytes
    File-->>OIS: Reads bytes
    OIS->>Reconstructed: readObject()

Based on the TU BCA syllabus for Object Oriented Programming in Java (CACS204), unit 15.

Discussion

Loading…