Serialization in Java

Definition:
Serialization is the process of converting an object’s state into a byte stream, so it can be easily saved to a file, transmitted over a network, or stored in a database. Deserialization is the reverse process of converting the byte stream back into a copy of the object.

Purpose:

  • To persist in the state of an object.
  • To transmit objects over a network between JVMs.
  • To perform deep cloning of objects.

Key Interfaces:

  • java.io.Serializable: A marker interface that identifies classes whose objects can be serialized.
  • java.io.Externalizable: An interface that extends Serializable and allows for customized serialization.

Basic Example of Serialization:

Java
import java.io.FileOutputStream;
import java.io.FileInputStream;
import java.io.ObjectOutputStream;
import java.io.ObjectInputStream;
import java.io.Serializable;
import java.io.IOException;

class Player implements Serializable {
    private static final long serialVersionUID = 1L;
    private String name;
    private int age;

    public Player(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public String toString() {
        return "Player{name='" + name + "', age=" + age + "}";
    }
}

public class SerializationExample {
    public static void main(String[] args) {
        Player player = new Player("John Doe", 25);

        // Serialization
        try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("player.ser"))) {
            out.writeObject(player);
            System.out.println("Player serialized: " + player);
        } catch (IOException e) {
            e.printStackTrace();
        }

        // Deserialization
        try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("player.ser"))) {
            Player deserializedPlayer = (Player) in.readObject();
            System.out.println("Player deserialized: " + deserializedPlayer);
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

Serialization in Java 21

Java 21 continues to support serialization with improvements and best practices for security and performance. Here are some notable aspects:

Enhanced Serialization Filter Mechanism:

Introduced in Java 9 and improved in subsequent versions, the serialization filter mechanism allows developers to define rules for filtering incoming serialization data. This is crucial for preventing deserialization vulnerabilities.
Example:

Java
import java.io.*;
import java.util.HashSet;
import java.util.Set;

public class SerializationFilterExample {
    public static void main(String[] args) {
        // Define a global serialization filter
        ObjectInputFilter filter = ObjectInputFilter.Config.createFilter("maxdepth=3;java.base/*;!*");
        ObjectInputFilter.Config.setSerialFilter(filter);

        // Serialization
        try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("data.ser"))) {
            Set<String> data = new HashSet<>();
            data.add("example");
            out.writeObject(data);
        } catch (IOException e) {
            e.printStackTrace();
        }

        // Deserialization with filtering
        try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("data.ser"))) {
            in.setObjectInputFilter(filter);
            Set<String> data = (Set<String>) in.readObject();
            System.out.println("Deserialized data: " + data);
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

Sealed Classes and Serialization

Java 17 introduced sealed classes, which can restrict which classes can extend or implement them.

In Java 21, sealed classes work seamlessly with serialization, ensuring only allowed subclasses are considered during deserialization.

Java
import java.io.*;

sealed class Sport implements Serializable permits Cricket, Tennis {
    private static final long serialVersionUID = 1L;
}

final class Cricket extends Sport {
    private static final long serialVersionUID = 1L;
    private String type;

    public Cricket(String type) {
        this.type = type;
    }

    @Override
    public String toString() {
        return "Cricket{type='" + type + "'}";
    }
}

final class Tennis extends Sport {
    private static final long serialVersionUID = 1L;
    private String courtType;

    public Tennis(String courtType) {
        this.courtType = courtType;
    }

    @Override
    public String toString() {
        return "Tennis{courtType='" + courtType + "'}";
    }
}

public class SealedSerializationExample {
    public static void main(String[] args) {
        Sport cricket = new Cricket("One Day International");

        // Serialization
        try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("sport.ser"))) {
            out.writeObject(cricket);
            System.out.println("Sport serialized: " + cricket);
        } catch (IOException e) {
            e.printStackTrace();
        }

        // Deserialization
        try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("sport.ser"))) {
            Sport deserializedSport = (Sport) in.readObject();
            System.out.println("Sport deserialized: " + deserializedSport);
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

Advanced Serialization Concepts

Custom Serialization

Sometimes, the default serialization mechanism is not sufficient, and we need custom serialization logic. This can be achieved by implementing writeObject and readObject methods.
Example:

Java
import java.io.*;

class AdvancedPlayer implements Serializable {
    private static final long serialVersionUID = 1L;
    private String name;
    private transient int age; // 'transient' to exclude from default serialization

    public AdvancedPlayer(String name, int age) {
        this.name = name;
        this.age = age;
    }

    private void writeObject(ObjectOutputStream out) throws IOException {
        out.defaultWriteObject();
        out.writeInt(age); // custom serialization logic for 'age'
    }

    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
        in.defaultReadObject();
        age = in.readInt(); // custom deserialization logic for 'age'
    }

    @Override
    public String toString() {
        return "AdvancedPlayer{name='" + name + "', age=" + age + "}";
    }
}

public class CustomSerializationExample {
    public static void main(String[] args) {
        AdvancedPlayer player = new AdvancedPlayer("Jane Doe", 28);

        // Serialization
        try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("advancedPlayer.ser"))) {
            out.writeObject(player);
            System.out.println("AdvancedPlayer serialized: " + player);
        } catch (IOException e) {
            e.printStackTrace();
        }

        // Deserialization
        try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("advancedPlayer.ser"))) {
            AdvancedPlayer deserializedPlayer = (AdvancedPlayer) in.readObject();
            System.out.println("AdvancedPlayer deserialized: " + deserializedPlayer);
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

Serialization Proxy Pattern

This pattern is used to avoid serialization issues by writing a serializable inner class to handle serialization instead of the outer class.
Example:

Java
import java.io.*;

class ComplexObject implements Serializable {
    private static final long serialVersionUID = 1L;
    private String data;

    public ComplexObject(String data) {
        this.data = data;
    }

    private Object writeReplace() {
        return new ComplexObjectProxy(this);
    }

    private static class ComplexObjectProxy implements Serializable {
        private static final long serialVersionUID = 1L;
        private final String data;

        ComplexObjectProxy(ComplexObject obj) {
            this.data = obj.data;
        }

        private Object readResolve() {
            return new ComplexObject(data);
        }
    }

    @Override
    public String toString() {
        return "ComplexObject{data='" + data + "'}";
    }
}

public class SerializationProxyExample {
    public static void main(String[] args) {
        ComplexObject obj = new ComplexObject("Complex Data");

        // Serialization
        try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("complexObject.ser"))) {
            out.writeObject(obj);
            System.out.println("ComplexObject serialized: " + obj);
        } catch (IOException e) {
            e.printStackTrace();
        }

        // Deserialization
        try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("complexObject.ser"))) {
            ComplexObject deserializedObj = (ComplexObject) in.readObject();
            System.out.println("ComplexObject deserialized: " + deserializedObj);
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

Conclusion

Serialization is a powerful feature in Java that allows objects to be converted to a byte stream and later restored. Java 21 continues to enhance serialization support with improved filtering mechanisms and seamless integration with newer language features like sealed classes. By understanding the basics, leveraging advanced techniques, and applying best practices, you can effectively utilize serialization in your Java applications. Including practical examples and exploring advanced concepts will provide a comprehensive learning experience for your audience.

Scroll to Top