浅谈Java序列化

基本介绍

Java序列化机制允许将一个Java对象的状态转换为字节流,以便可以将其保存到磁盘或在网络上进行传输。稍后,这些字节流可以被反序列化以重建原来的对象。这个机制在远程方法调用(RMI)、Java Beans,以及持久化等多种情景中非常有用。

基本原理

  1. 序列化(Serialization):是将对象的状态信息转换为可以存储或传输的形式的过程。在Java中,序列化的对象必须实现 java.io.Serializable 接口。这个接口是一个标记接口,它不包含任何方法。

  2. 反序列化(Deserialization):是将已存储的数据(如文件,数据库)或通过网络接收的字节流还原为对象的过程。

实现序列化

要让一个Java对象可序列化,需要实现 Serializable 接口。例如:

public class User implements Serializable {
    private String name;
    private transient int age; // 使用 transient 关键字标记的字段不会被序列化

    // 构造器、getter、setter等
}

在上面的例子中,User 类通过实现 Serializable 接口变成可序列化的。transient 关键字用于防止字段被序列化。

序列化的过程

序列化是通过 ObjectOutputStream 类实现的。例如:

User user = new User("John", 30);
try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("user.ser"))) {
    out.writeObject(user);
}

反序列化的过程

反序列化是通过 ObjectInputStream 类实现的。例如:

User user;
try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("user.ser"))) {
    user = (User) in.readObject();
}

特殊处理

  • transient 关键字:如果不想某个字段被序列化,可以使用 transient 关键字标记它。
  • 版本控制:通过 serialVersionUID 字段提供序列化版本控制。如果序列化对象的版本与反序列化时的版本不匹配,将抛出 InvalidClassException
  • 自定义序列化:可以通过重写 writeObjectreadObject 方法来自定义序列化过程。
  • 使用 readResolvewriteReplace 方法:这些方法允许在序列化和反序列化过程中进行特殊的操作,比如维护单例模式。

序列化的用途

  • 持久化:将对象的状态保存到存储媒介(如硬盘)。
  • 深复制:通过序列化和反序列化创建对象的深拷贝。
  • 通信:在网络中传输对象的状态,例如在客户端和服务器之间。

注意事项

  • 并非所有的对象都可以序列化。对象的所有属性以及属性的属性都必须是可序列化的。
  • 静态字段不会被序列化,因为它们属于类的状态,而不是对象的状态。
  • 序列化可能引发性能问题和安全问题,因此需要谨慎使用。

Java序列化机制是一种强大的工具,但同时也需要理解它的工作原理和适用场景,以便正确和高效地使用。

readResolve 和 writeReplace 方法

在Java序列化机制中,readResolvewriteReplace 方法用于自定义对象的序列化和反序列化过程。这些方法在java.io.Serializable接口中并不是必需的,但如果类中定义了它们,它们会在序列化和反序列化过程中自动被调用。

writeReplace 方法

writeReplace 方法用于序列化过程。当一个对象被序列化时,如果该对象的类定义了 writeReplace 方法,这个方法会被调用,并且它返回的对象会代替原始对象被序列化。

使用场景
  • 替换序列化对象:在序列化之前,可能需要替换为另一个对象。例如,当序列化代理模式(Serialization Proxy Pattern)被使用时,可以通过 writeReplace 方法返回一个代理类的实例。
  • 保护数据:在序列化敏感数据前,可以用 writeReplace 来替换或过滤数据。
示例
public class SensitiveData implements Serializable {
    private String sensitiveInfo;

    public SensitiveData(String info) {
        this.sensitiveInfo = info;
    }

    // 这个方法在序列化之前被调用
    private Object writeReplace() throws ObjectStreamException {
        // 返回一个替代对象进行序列化
        return new SafeDataProxy("Data is Protected");
    }

    private static class SafeDataProxy implements Serializable {
        private String safeInfo;

        public SafeDataProxy(String safeInfo) {
            this.safeInfo = safeInfo;
        }
    }
}

在上面的例子中,writeReplace 方法返回了一个内部静态类 SafeDataProxy 的实例。这个代理类不包含任何敏感信息。当 SensitiveData 类的对象被序列化时,实际上序列化的是 SafeDataProxy 对象。

readResolve 方法

readResolve 方法用于反序列化过程。当一个对象被反序列化时,如果其类定义了 readResolve 方法,这个方法会在反序列化后立即被调用,它返回的对象会替换从流中读取的对象。

使用场景
  • 维护单例属性:在单例模式下,防止反序列化创建新的实例。
  • 替换反序列化对象:在反序列化后,可能需要替换为另一个对象,例如恢复代理对象为原始对象。
示例
public class Singleton implements Serializable {
    private static final long serialVersionUID = 1L;

    // 类的唯一实例
    private static final Singleton INSTANCE = new Singleton();

    // 私有构造函数,防止外部实例化
    private Singleton() {}

    // 提供一个全局访问点
    public static Singleton getInstance() {
        return INSTANCE;
    }

    // readResolve 方法,用于保持单例属性
    private Object readResolve() throws ObjectStreamException {
        // 返回已经存在的单例实例,而不是反序列化产生的新实例
        return INSTANCE;
    }
}

在上面的例子中,Singleton.INSTANCE 是单例模式中唯一的实例。这确保了即使在序列化和反序列化过程中,也始终只有这一个实例。

注意事项

  • 这些方法必须声明为 private,以避免外部调用。
  • 它们必须返回 Object 类型。
  • 在实现时要注意线程安全和对象一致性问题。
  • 这些方法不是 Serializable 接口的一部分,但如果它们存在于实现了 Serializable 接口的类中,序列化机制会自动识别和使用它们。

通过使用 writeReplacereadResolve 方法,开发者可以有效地控制和自定义Java对象的序列化和反序列化行为。

writeObject 和 readObject 方法

当需要对Java对象的序列化过程进行更详细的控制时,可以通过重写 writeObjectreadObject 方法来自定义序列化和反序列化过程。这对于处理复杂对象或需要特别处理的成员变量(如那些不支持默认序列化的变量)特别有用。

示例:自定义序列化和反序列化过程

假设我们有一个 Person 类,其中包含一些基本信息和一个不可序列化的字段(如线程)。我们可以自定义序列化和反序列化过程来处理这个不可序列化的字段。

package per.mjn.bean;

import java.io.*;

class Employee implements Serializable {
    private String name;
    private int age;
    private transient String socialSecurityNumber; // 敏感信息,不希望序列化

    public Employee(String name, int age, String ssn) {
        this.name = name;
        this.age = age;
        this.socialSecurityNumber = ssn;
    }

    private void writeObject(ObjectOutputStream oos) throws IOException {
        oos.defaultWriteObject(); // 默认序列化处理
        // 自定义序列化逻辑
        // 在这里,我们选择不序列化社会保险号码
    }

    private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
        ois.defaultReadObject(); // 默认反序列化处理
        // 自定义反序列化逻辑
        // 因为社会保险号码没有被序列化,我们可能需要在这里进行某些操作,如设置一个默认值
        this.socialSecurityNumber = "未知";
    }

    // Getters and Setters
    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }

    public String getSocialSecurityNumber() {
        return socialSecurityNumber;
    }
}


public class Main {
    public static void main(String[] args) {
        // 创建 Employee 对象
        Employee employee = new Employee("John Doe", 30, "123-45-6789");

        // 序列化到文件
        try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("employee.ser"))) {
            oos.writeObject(employee);
            System.out.println("Employee object serialized.");
        } catch (IOException e) {
            e.printStackTrace();
        }

        // 反序列化从文件
        try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("employee.ser"))) {
            Employee deserializedEmployee = (Employee) ois.readObject();
            System.out.println("Employee object deserialized.");
            System.out.println("Name: " + deserializedEmployee.getName() + ", Age: " + deserializedEmployee.getAge()
                + ", socialSecurityNumber: " + deserializedEmployee.getSocialSecurityNumber());
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

在上面的例子中:

  • writeObject 方法:用于自定义对象的序列化过程。首先,我们调用 defaultWriteObject 来处理默认的序列化。然后,可以添加任何特殊的序列化逻辑。

  • readObject 方法:用于自定义对象的反序列化过程。首先,我们调用 defaultReadObject 来处理默认的反序列化。然后,可以添加任何特殊的反序列化逻辑,例如重新初始化 transient 字段。

程序运行结果如下:

Employee object serialized.
Employee object deserialized.
Name: John Doe, Age: 30, socialSecurityNumber: 未知

注意事项

  • 这些方法应该是 private 的,以确保它们不会被类的外部代码调用。

  • writeObjectreadObject中调用defaultWriteObjectdefaultReadObject是重要的,因为这些方法处理那些没有特殊处理需求的字段。

  • 通过这种方式,我们可以细粒度地控制哪些字段如何序列化,以及在反序列化时如何恢复对象的状态。

这种方法提供了对Java对象序列化和反序列化过程的高级控制,允许开发者处理复杂的序列化场景,或在序列化过程中包含额外的逻辑。


静态字段不参与Java的序列化过程

静态字段不参与Java的序列化过程,原因在于静态字段不是对象实例的一部分,而是属于类的状态。在Java中,序列化的主要目的是保存对象的状态(即实例变量),以便可以在以后准确地重建该对象的实例。以下是不序列化静态字段的几个关键原因:

  1. 类级别的状态:静态字段属于类级别的状态,而不是对象级别的状态。它们在类加载时初始化,并且对该类的所有实例共享。序列化的目的是存储和恢复对象的个体状态,而静态字段的状态跨所有实例共享。

  2. 内存和类加载器:静态字段的值存储在方法区或类区域中,并且由类加载器管理。这意味着它们的生命周期与应用程序的类加载器相关,而不是与单个对象实例相关。

  3. 数据一致性:假设静态字段可以被序列化和反序列化,那么这可能导致数据一致性问题。因为静态字段是共享的,序列化和随后的反序列化可能会在不同时间点导致类的不同实例看到该静态字段的不同状态。

  4. 设计哲学:Java序列化的设计哲学是为了支持对象的持久性和远程通信。在这两种情况下,关注的是对象的当前状态,而不是类级别的静态信息。

  5. 安全性和封装性:静态字段通常用于维护类的内部状态和全局配置。允许它们通过序列化进行修改可能会破坏封装性,导致安全和维护问题。

因此,静态字段不被序列化,也意味着反序列化对象时不会影响这些字段的状态。它们的值将由当前JVM中的类定义或静态初始化器所确定。

你可能感兴趣的:(Java基础,java)