Blog

Java Serialization in Depth

Preface: Java serialization has both performance and security implications. While it allows any object to be converted into a byte stream, alternatives like Protocol Buffers and Jackson JSON are often preferred for various reasons.

For information on how JSON serialization works, check out Java Object Mapper, What it is, how it works.


Java is an object-oriented language. You define classes like this:

public class User {
    private String name;
    private String email;

    public void setName(String name){
        this.name = name;
    }

    public String getName(){
        return this.name;
    }
}

and create objects like this:

User user = new User();
user.setName("Sam");
user.getName(); //Sam

While the Java runtime (JVM) understands this structure, other environments may not. What if you want to save a user to a database? What if you want to write that user to a file or transfer it over a network?

Serialization makes this possible by converting a Java class into a series of bytes that another system can reconstruct into the original object. The process of writing an object to a byte sequence is serialization, while reading those bytes back into an object is deserialization.

Key Takeaways

  • Serialization converts Java objects into byte streams for storage or transmission.
  • Java's Serializable interface marks classes for serialization.
  • serialVersionUID ensures class compatibility during deserialization.
  • Serialization involves security and performance trade-offs.
  • Alternatives to Java's serialization include Protocol Buffers and JSON.

What is serialization?

Serialization involves converting Java objects into data formats that other systems can interpret. For instance, a database understands byte sequences rather than object instantiations like this:

new User()

A byte representation might look like this:

0000000    edac    0500    7273    1500    6f63    2e6d    7865    6d61
0000010    6c70    2e65    6564    6f6d    552e    6573    6372    ca34
0000020    73ee    0345    029d    0200    004c    7008    7361    7773
0000030    726f    7464    1200    6a4c    7661    2f61    616c    676e
0000040    532f    7274    6e69    3b67    004c    7508    6573    6e72
0000050    6d61    7165    7e00    0100    7078    7070
000005c

Serialization isn't unique to Java—languages like JavaScript and PHP use it too. Here's how JavaScript handles JSON deserialization:

var obj = JSON.parse('{ "name":"Sam", "email":"sam@mail.com"}');

Why do we need serialization in Java?

Serialization is crucial for handling data transfer across diverse systems. Without it, data transmission from a web server to a web browser, or from a Java entity to a database, wouldn't be feasible.

It's the bridge between the POJOs you define and their stored or transmitted representations. Serialization enables system interoperability, allowing data to be preserved and shared effectively.

Serialization in Java: How it works

The java.io package offers classes to serialize objects, like ObjectOutputStream:

FileOutputStream fos = new FileOutputStream("temp.out");
ObjectOutputStream oos = new ObjectOutputStream(fos);
User user = new User();
oos.writeObject(user);
oos.flush();
oos.close();

Here's the serialization process:

  1. Metadata about the object's class and its superclasses is serialized.
  2. Non-static, non-transient instance data is serialized.
  3. Members' metadata is serialized, similar to step 1.
  4. Members' data is serialized, similar to step 2.

ObjectOutputStream uses FileOutputStream to handle stream data. For more on Java I/O basics, see FileReader vs BufferedReader vs Scanner.

Deserialization uses ObjectInputStream:

FileInputStream fis = new FileInputStream("temp.out");
ObjectInputStream oin = new ObjectInputStream(fis);
try {
    User user = (User) oin.readObject();
} catch(Exception e) {
    //handle exception
}

The deserialization process reads metadata to reconstruct the object accurately.

The Java Serializable Interface

The User class must implement Serializable:

public class User implements Serializable

If not, you'll see this exception:

Exception in thread "main" java.io.NotSerializableException: com.example.demo.User

Implementing Serializable indicates a class can be serialized. It contains no methods—it's a marker interface. Members of a class, such as:

public class User {
    private Address address;
}

must also be serializable for the entire object to be successfully serialized.

SerialVersionUID

Each Serializable class has a unique identifier for versioning, known as serialVersionUID:

public class User implements Serializable {
    private static final long serialVersionUID = 7148561634028749725L;
    ...

Defining this explicitly is critical to avoid inconsistencies across different Java implementations.

Problems with Serialization?

Serialization can expose private data through unintended access to non-transient private members. Without explicit serialVersionUID, changes in class definitions over time can lead to deserialization issues.

Although Java's serialization is versatile, alternatives like Protocol Buffers or Jackson can offer better solutions depending on your use case.

Java Serialization Example

package com.example.demo;

import java.io.Serializable;

public class User implements Serializable {
    private static final long serialVersionUID = 7148561634028749725L;
    private String username;
    private transient String password;


    public void setUserName(String username){
        this.username = username;
    }

    public String getUserName(){
        return this.username;
    }

    public void setPassword(String password){
        this.password = password;
    }

    public String getPassword(){
        return this.password;
    }

}
package com.example.demo;

import java.io.*;

public class DemoApplication {

    public static void main(String[] args) throws IOException {

        FileOutputStream fos = new FileOutputStream("temp.out");
        ObjectOutputStream oos = new ObjectOutputStream(fos);
        User user = new User();

        user.setUserName("Sam");
        user.setPassword(("password123"));

        oos.writeObject(user);
        oos.flush();
        oos.close();

        FileInputStream fis = new FileInputStream("temp.out");
        ObjectInputStream oin = new ObjectInputStream(fis);
        try {
            User userFromFile = (User) oin.readObject();
            System.out.println(userFromFile.getUserName()); //Sam
            System.out.println(userFromFile.getPassword()); //null because transient field isn't serialized.
        } catch(Exception e) {
            //handle exception
        }
    }

}

The User class implements the Serializable interface with a serialVersionUID, while the transient keyword prevents password from being serialized, resulting in null during deserialization.

FAQ

Why is serialVersionUID important?

It ensures that a serialized object corresponds to a serialized class. Without defining it, changes in class details can cause deserialization errors.

Can serialization be a security risk?

Yes, serialized data can be tampered with, leading to deserialization vulnerabilities. Use alternatives or implement security checks where needed.

When should I use alternatives to Java serialization?

Consider alternatives like Protocol Buffers when efficiency, cross-language communication, or forward/backward compatibility is crucial.

What does transient mean?

The transient keyword prevents fields from being serialized, useful for sensitive data or non-essential fields.

Mastering the tech interviewWhat everyone is doing wrong in tech interviews