MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

如何自定义Java对象的序列化与反序列化

2023-04-147.3k 阅读

一、Java 序列化基础回顾

在深入探讨自定义序列化与反序列化之前,先来回顾一下 Java 序列化的基础知识。Java 的序列化机制允许将一个对象转换为字节序列,以便在网络上传输或持久化到存储设备(如文件)中。之后,可以通过反序列化将这些字节序列恢复成原始的对象。

Java 提供了 java.io.Serializable 接口来标记一个类是可序列化的。如果一个类实现了这个接口,那么该类的对象就可以被序列化。例如:

import java.io.Serializable;

public class User implements Serializable {
    private String name;
    private int age;

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

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }
}

在上述代码中,User 类实现了 java.io.Serializable 接口,因此 User 类的对象可以被序列化。

要进行序列化操作,通常会使用 ObjectOutputStream 类,而反序列化则使用 ObjectInputStream 类。下面是一个简单的序列化与反序列化示例:

import java.io.*;

public class SerializationExample {
    public static void main(String[] args) {
        User user = new User("Alice", 30);

        // 序列化
        try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("user.ser"))) {
            oos.writeObject(user);
        } catch (IOException e) {
            e.printStackTrace();
        }

        // 反序列化
        try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("user.ser"))) {
            User deserializedUser = (User) ois.readObject();
            System.out.println("Deserialized User: Name - " + deserializedUser.getName() + ", Age - " + deserializedUser.getAge());
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

上述代码首先创建了一个 User 对象并将其序列化到文件 user.ser 中,然后从该文件中反序列化出 User 对象并打印其属性。

二、为什么需要自定义序列化与反序列化

  1. 默认序列化的局限性

    • 敏感信息暴露:默认的序列化机制会将对象的所有字段(包括敏感信息,如密码、密钥等)都进行序列化。例如,如果 User 类中有一个 password 字段,在默认序列化时这个字段也会被写入到序列化数据中,这在安全性要求较高的场景下是不可接受的。
    • 性能问题:对于一些复杂对象,默认序列化可能会序列化一些不必要的字段,导致序列化数据量过大,从而影响网络传输性能或存储效率。比如一个包含大量缓存数据的对象,这些缓存数据在反序列化后可能不需要重新构建,默认序列化却会将其包含在内。
    • 兼容性问题:当类的结构发生变化时,默认序列化可能会导致反序列化失败。例如,增加或删除了字段,默认序列化机制可能无法正确处理这些变化。
  2. 自定义的优势

    • 安全控制:通过自定义序列化,可以选择性地序列化字段,避免敏感信息被序列化。
    • 性能优化:只序列化必要的字段,减少数据量,提高序列化和反序列化的效率。
    • 兼容性增强:可以更好地处理类结构变化,通过自定义逻辑在反序列化时对新老版本的数据进行适配。

三、自定义序列化的实现方式

  1. 使用 transient 关键字
    • 原理transient 关键字用于标记一个字段不参与默认的序列化过程。当一个对象被序列化时,被 transient 修饰的字段不会被写入到序列化数据中。例如,修改 User 类如下:
import java.io.Serializable;

public class User implements Serializable {
    private String name;
    private int age;
    private transient String password;

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

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }

    public String getPassword() {
        return password;
    }
}

在上述代码中,password 字段被 transient 修饰,在序列化 User 对象时,password 字段的值不会被写入到序列化数据中。

  • 局限性:虽然 transient 关键字可以简单地排除某些字段不被序列化,但它的功能较为单一,无法满足更复杂的自定义序列化需求,比如对某些字段进行加密后再序列化等。
  1. 实现 writeObject 和 readObject 方法
    • 原理:如果一个类实现了 java.io.Serializable 接口,并且定义了 private void writeObject(java.io.ObjectOutputStream out)private void readObject(java.io.ObjectInputStream in) 方法,Java 的序列化机制会调用这些方法来进行自定义的序列化和反序列化操作。
    • 示例
import java.io.*;

public class CustomUser implements Serializable {
    private String name;
    private int age;
    private String secretData;

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

    private void writeObject(ObjectOutputStream out) throws IOException {
        // 先写入常规字段
        out.writeUTF(name);
        out.writeInt(age);
        // 对敏感数据进行加密后写入
        String encryptedData = encrypt(secretData);
        out.writeUTF(encryptedData);
    }

    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
        name = in.readUTF();
        age = in.readInt();
        String encryptedData = in.readUTF();
        // 读取后解密
        secretData = decrypt(encryptedData);
    }

    private String encrypt(String data) {
        // 简单的加密示例,实际应用中应使用更安全的加密算法
        StringBuilder encrypted = new StringBuilder();
        for (char c : data.toCharArray()) {
            encrypted.append((char) (c + 1));
        }
        return encrypted.toString();
    }

    private String decrypt(String encryptedData) {
        StringBuilder decrypted = new StringBuilder();
        for (char c : encryptedData.toCharArray()) {
            decrypted.append((char) (c - 1));
        }
        return decrypted.toString();
    }
}

在上述代码中,CustomUser 类定义了 writeObjectreadObject 方法。在 writeObject 方法中,先将 nameage 字段按常规方式写入,然后对 secretData 进行加密后写入。在 readObject 方法中,按顺序读取数据,并对加密的 secretData 进行解密。

四、自定义序列化与版本控制

  1. serialVersionUID 的作用
    • 原理serialVersionUID 是一个类的序列化版本标识符。当一个类实现了 Serializable 接口时,如果没有显式定义 serialVersionUID,Java 会根据类的结构自动生成一个 serialVersionUID。但这种自动生成的 serialVersionUID 对类结构的变化非常敏感,类的任何微小变化(如增加或删除一个字段)都会导致 serialVersionUID 改变,从而使得反序列化失败。
    • 显式定义 serialVersionUID:为了更好地控制序列化版本兼容性,建议显式定义 serialVersionUID。例如:
import java.io.Serializable;

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

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

在上述代码中,显式定义了 serialVersionUID1L。即使类的结构发生变化,只要 serialVersionUID 不变,反序列化时就有可能成功(取决于具体的变化情况和自定义序列化逻辑)。

  1. 处理类结构变化
    • 增加字段:如果在类中增加了新的字段,在反序列化老版本数据时,新字段会采用其默认值。例如,在 VersionedUser 类中增加一个新字段 email
import java.io.Serializable;

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

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

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

    // getters and setters
    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }
}

当反序列化老版本的 VersionedUser 对象时,email 字段的值将为 null

  • 删除字段:删除字段可能会导致反序列化失败,除非在 readObject 方法中进行特殊处理。例如,如果删除了 age 字段,可以在 readObject 方法中读取 age 字段但不使用它,从而保持兼容性。

五、自定义序列化中的继承与多态

  1. 继承关系下的序列化
    • 父类与子类的序列化:如果父类实现了 Serializable 接口,子类自动继承可序列化的特性。例如:
import java.io.Serializable;

class Animal implements Serializable {
    private String species;

    public Animal(String species) {
        this.species = species;
    }

    public String getSpecies() {
        return species;
    }
}

class Dog extends Animal {
    private String name;

    public Dog(String species, String name) {
        super(species);
        this.name = name;
    }

    public String getName() {
        return name;
    }
}

在上述代码中,Animal 类实现了 Serializable 接口,Dog 类继承自 Animal,因此 Dog 类的对象也可以被序列化。在序列化 Dog 对象时,Animal 类的 species 字段和 Dog 类的 name 字段都会被序列化。

  • 自定义父类的序列化方法:如果父类有自定义的 writeObjectreadObject 方法,子类在进行自定义序列化时需要注意调用父类的相应方法。例如:
import java.io.*;

class Parent implements Serializable {
    private int value;

    public Parent(int value) {
        this.value = value;
    }

    private void writeObject(ObjectOutputStream out) throws IOException {
        out.writeInt(value);
    }

    private void readObject(ObjectInputStream in) throws IOException {
        value = in.readInt();
    }
}

class Child extends Parent {
    private String text;

    public Child(int value, String text) {
        super(value);
        this.text = text;
    }

    private void writeObject(ObjectOutputStream out) throws IOException {
        // 先调用父类的 writeObject 方法
        out.defaultWriteObject();
        out.writeUTF(text);
    }

    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
        // 先调用父类的 readObject 方法
        in.defaultReadObject();
        text = in.readUTF();
    }
}

在上述代码中,Child 类在 writeObjectreadObject 方法中先调用了 defaultWriteObjectdefaultReadObject 方法(这两个方法会调用父类的相应序列化方法),然后再处理自身特有的字段。

  1. 多态与序列化
    • 序列化多态对象:当序列化一个多态对象时,Java 会序列化对象的实际类型信息。例如:
import java.io.*;

class Shape implements Serializable {
    private String color;

    public Shape(String color) {
        this.color = color;
    }

    public String getColor() {
        return color;
    }
}

class Circle extends Shape {
    private double radius;

    public Circle(String color, double radius) {
        super(color);
        this.radius = radius;
    }

    public double getRadius() {
        return radius;
    }
}

public class PolymorphismSerialization {
    public static void main(String[] args) {
        Shape shape = new Circle("Red", 5.0);

        // 序列化
        try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("shape.ser"))) {
            oos.writeObject(shape);
        } catch (IOException e) {
            e.printStackTrace();
        }

        // 反序列化
        try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("shape.ser"))) {
            Shape deserializedShape = (Shape) ois.readObject();
            if (deserializedShape instanceof Circle) {
                Circle circle = (Circle) deserializedShape;
                System.out.println("Deserialized Circle: Color - " + circle.getColor() + ", Radius - " + circle.getRadius());
            } else {
                System.out.println("Deserialized Shape: Color - " + deserializedShape.getColor());
            }
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

在上述代码中,将一个 Circle 对象赋值给 Shape 类型的变量并进行序列化。反序列化时,通过 instanceof 关键字判断对象的实际类型,从而可以正确处理多态对象。

六、自定义序列化的高级应用

  1. 与外部库的集成
    • JSON 序列化:在某些场景下,可能需要将 Java 对象序列化为 JSON 格式。虽然 Java 有许多 JSON 处理库(如 Jackson、Gson 等),但也可以结合自定义序列化来实现特定需求。例如,使用 Jackson 库,可以自定义序列化逻辑来处理复杂对象的 JSON 序列化。
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.ser.std.StdSerializer;

import java.io.IOException;

public class CustomObjectSerializer extends StdSerializer<CustomObject> {
    public CustomObjectSerializer() {
        this(null);
    }

    public CustomObjectSerializer(Class<CustomObject> t) {
        super(t);
    }

    @Override
    public void serialize(CustomObject value, JsonGenerator gen, SerializerProvider provider) throws IOException {
        gen.writeStartObject();
        gen.writeStringField("customField1", value.getCustomField1());
        // 对某些字段进行特殊处理
        String encryptedField = encrypt(value.getSecretField());
        gen.writeStringField("encryptedSecretField", encryptedField);
        gen.writeEndObject();
    }

    private String encrypt(String data) {
        // 简单加密逻辑
        return new StringBuilder(data).reverse().toString();
    }
}

在上述代码中,定义了一个自定义的 Jackson 序列化器 CustomObjectSerializer,对 CustomObject 类进行自定义 JSON 序列化,对敏感字段进行加密处理。

  • 数据库持久化:在将 Java 对象持久化到数据库时,自定义序列化可以帮助将复杂对象转换为适合数据库存储的格式。例如,对于一个包含嵌套对象的 Java 对象,可以通过自定义序列化将其转换为 JSON 字符串存储在数据库的文本字段中,在读取时再进行反序列化。
  1. 分布式系统中的应用
    • 跨节点传输:在分布式系统中,对象需要在不同节点之间传输。自定义序列化可以减少传输数据量,提高传输效率。例如,在一个分布式缓存系统中,缓存的对象可能包含一些本地计算的临时数据,这些数据在其他节点不需要,通过自定义序列化可以排除这些数据,只传输必要的字段。
    • 一致性维护:在分布式系统中,不同节点上的对象版本可能不一致。通过自定义序列化和版本控制(如 serialVersionUID),可以更好地处理对象版本差异,确保系统的一致性。例如,当一个节点上的对象结构发生变化时,通过自定义反序列化逻辑可以将老版本的数据转换为新版本的对象,而不会导致系统出错。

通过以上对自定义 Java 对象序列化与反序列化的详细阐述,包括基础回顾、自定义的原因、实现方式、版本控制、继承与多态以及高级应用等方面,希望能帮助开发者在实际项目中根据需求灵活运用自定义序列化,解决各种与对象持久化和传输相关的问题。