杰瑞科技汇

Java serializable,如何正确实现与使用?

Of course! Let's dive deep into Java's Serializable interface. It's a fundamental concept in Java for object persistence and data transfer.

Java serializable,如何正确实现与使用?-图1
(图片来源网络,侵删)

What is Serializable?

In simple terms, Serializable is a marker interface in Java. A marker interface is an interface with no methods or fields. Its sole purpose is to mark or "tag" a class so that the JVM (Java Virtual Machine) can give it special treatment.

When a class implements Serializable, it tells the JVM: "An object of this class can be converted into a sequence of bytes (serialized) and, later, this sequence of bytes can be used to recreate the object (deserialized)."

This process of converting an object to a byte stream is called Serialization. The reverse process is called Deserialization.


Why is it Important?

Serialization is crucial for several common scenarios:

Java serializable,如何正确实现与使用?-图2
(图片来源网络,侵删)
  1. Persistence: Saving an object's state to a file or a database so that it can be recreated later, even after the program has terminated.
  2. Network Communication (RMI, EJB, etc.): Sending an object's data over a network from one Java Virtual Machine (JVM) to another. The object is serialized on the sender's side, transmitted as bytes, and deserialized on the receiver's side.
  3. Caching: Storing an object in a memory cache (like Redis or Memcached) to avoid expensive re-computation or re-fetching.
  4. Deep Cloning: Creating a deep copy of an object by serializing it and then immediately deserializing it.

How to Use Serializable

Using Serializable is surprisingly simple.

Step 1: Make Your Class Implement the Interface

Just add implements Serializable to your class declaration. That's it!

import java.io.Serializable;
public class User implements Serializable {
    // This is a recommended best practice. See the 'versioning' section below.
    private static final long serialVersionUID = 1L;
    private String username;
    private transient String password; // See the 'transient' keyword section below
    private int age;
    // Constructor, Getters, and Setters
    public User(String username, String password, int age) {
        this.username = username;
        this.password = password;
        this.age = age;
    }
    @Override
    public String toString() {
        return "User{" +
                "username='" + username + '\'' +
                ", password='" + (password != null ? "*****" : "null") + '\'' +
                ", age=" + age +
                '}';
    }
}

Step 2: Use ObjectOutputStream to Serialize

To write an object to a file, you use ObjectOutputStream.

import java.io.*;
public class SerializationExample {
    public static void main(String[] args) {
        User user = new User("john_doe", "secret123", 30);
        try (ObjectOutputStream oos = new ObjectOutputStream(
                new FileOutputStream("user.ser"))) { // .ser is a common extension for serialized files
            // The magic happens here!
            oos.writeObject(user);
            System.out.println("User object has been serialized to user.ser");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

After running this, a file named user.ser will be created in your project's root directory. If you open it, it will look like gibberish because it's a binary format, not text.

Java serializable,如何正确实现与使用?-图3
(图片来源网络,侵删)

Step 3: Use ObjectInputStream to Deserialize

To read the object back from the file, you use ObjectInputStream.

import java.io.*;
public class DeserializationExample {
    public static void main(String[] args) {
        User deserializedUser;
        try (ObjectInputStream ois = new ObjectInputStream(
                new FileInputStream("user.ser"))) {
            // The magic happens here!
            deserializedUser = (User) ois.readObject(); // Note the cast!
            System.out.println("User object has been deserialized.");
            System.out.println("Deserialized User: " + deserializedUser);
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

Output:

User object has been deserialized.
Deserialized User: User{username='john_doe', password='null', age=30}

Notice that the password field is null. This is because of the transient keyword, which we'll discuss next.


Key Concepts and Best Practices

The serialVersionUID

This is one of the most important concepts in serialization.

  • What it is: A serialVersionUID is a unique identifier for each Serializable class. It's a static final long field.
  • Why it's needed: During deserialization, the JVM checks the serialVersionUID of the incoming object against the serialVersionUID of the class definition in the JVM's memory.
    • If they match, the object is deserialized successfully.
    • If they do not match, the JVM throws an InvalidClassException. This is a safety mechanism to prevent you from deserializing data from a different version of the class, which could lead to corrupted objects or runtime errors.
  • Best Practice: Always explicitly declare a serialVersionUID in your Serializable classes. If you don't, the JVM will generate one automatically based on the class's structure (name, fields, methods, etc.). This is dangerous because any small change to the class (like adding a new field) will change the generated UID, making previously serialized objects incompatible.
// Best Practice
private static final long serialVersionUID = 1L; // Or a more complex number

The transient Keyword

The transient keyword is used to mark a field so that the serialization mechanism will ignore it. Its value will not be saved to the byte stream.

  • Common Use Cases:
    • Security: Storing sensitive data like passwords, credit card numbers, or encryption keys. You don't want these to be saved to a file or sent over the network in plain text.
    • Non-Serializable State: A field that holds a state that cannot or should not be serialized. For example, a Thread object or a file handle (FileInputStream).

In our User example, password was marked as transient. When we deserialized the object, the password field was null because it was never written to the file.

Customizing Serialization: writeObject() and readObject()

Sometimes, the default serialization isn't enough. You might need to perform logic before or after the process. You can achieve this by providing private methods in your class:

  • private void writeObject(ObjectOutputStream oos) throws IOException
  • private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException

These methods are called automatically by the ObjectOutputStream and ObjectInputStream respectively.

Example: Let's say we want to encrypt the password before serialization and decrypt it after.

import java.io.*;
public class UserWithCustomSerialization implements Serializable {
    private static final long serialVersionUID = 2L;
    private String username;
    private transient String password; // Still transient, but we'll handle it manually
    private int age;
    // ... constructor, getters, setters ...
    // Custom serialization logic
    private void writeObject(ObjectOutputStream oos) throws IOException {
        // Default serialization for non-transient fields
        oos.defaultWriteObject();
        // Manually write the transient field (e.g., after encryption)
        String encryptedPassword = "ENCRYPTED:" + this.password; // Simplified "encryption"
        oos.writeObject(encryptedPassword);
        System.out.println("Custom writeObject: Password encrypted and written.");
    }
    // Custom deserialization logic
    private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
        // Default deserialization for non-transient fields
        ois.defaultReadObject();
        // Manually read the transient field (e.g., for decryption)
        String encryptedPassword = (String) ois.readObject();
        this.password = encryptedPassword.replace("ENCRYPTED:", "");
        System.out.println("Custom readObject: Password decrypted and restored.");
    }
    @Override
    public String toString() {
        return "UserWithCustomSerialization{" +
                "username='" + username + '\'' +
                ", password='" + password + '\'' +
                ", age=" + age +
                '}';
    }
}

When you use this class, the writeObject and readObject methods will be called automatically, giving you full control over the serialization process.


What About Inheritance?

  • If a superclass implements Serializable, all its subclasses automatically do as well.
  • If a superclass does not implement Serializable, the subclass can still implement it. During deserialization, the superclass's no-argument constructor will be called to initialize its part of the object. If the superclass doesn't have a no-arg constructor, you'll get an InvalidClassException.

Alternatives to Java Serialization

While Serializable is built-in, it has some drawbacks:

  • Performance: It can be slow and produces large byte streams.
  • Security: It can be a security risk if you deserialize data from an untrusted source (a vulnerability known as "deserialization of untrusted data").
  • Verbosity: It's not human-readable.

Because of these, modern Java applications often use more robust and efficient alternatives:

  • JSON (JavaScript Object Notation): The de-facto standard for web APIs. Libraries like Jackson, Gson, and org.json are extremely popular.
  • XML (eXtensible Markup Language): Older, verbose, but still used in some enterprise and configuration systems (e.g., with JAXB).
  • Protocol Buffers (Protobuf) / FlatBuffers: Google's high-performance, binary data formats. They require a schema definition (.proto file) but are much faster and smaller than Java serialization. They are widely used in microservices and mobile apps.

Summary

Feature Description
Purpose To convert an object into a byte stream (serialization) and back (deserialization).
How to Use Implement the Serializable marker interface in your class.
serialVersionUID A unique ID for version control. Always define it explicitly.
transient A keyword to exclude a field from serialization.
Customization Use private writeObject() and readObject() methods for custom logic.
Alternatives JSON (Jackson/Gson), XML (JAXB), Protobuf are often preferred for performance and interoperability.
分享:
扫描分享到社交APP
上一篇
下一篇