Serializable接口和Externalizable接口都用于支持对象的序列化,但它们在实现方式和使用方法上有一些区别。

Serializable接口

默认序列化机制

Serializable接口是Java提供的标准序列化机制,当一个类实现了Serializable接口时,它的所有非transient成员变量都会自动进行序列化。

简单使用

实现Serializable接口的类无需实现任何方法,因为它是一个标记接口,Java运行时系统会自动对对象进行序列化和反序列化。

序列化机制不透明

Serializable接口的序列化机制是不透明的,即无法直接访问序列化的数据或者修改序列化的过程。

Externalizable接口

手动控制序列化过程

Externalizable接口提供了更加灵活的序列化机制,允许开发者手动控制对象的序列化和反序列化过程。

需要实现方法

实现Externalizable接口的类需要实现writeExternalreadExternal两个方法,分别用于手动指定对象的序列化和反序列化过程。

下面是一个使用Externalizable接口实现自定义序列化和反序列化过程的示例:

import java.io.*;

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

    // 必须提供默认构造函数,因为在反序列化时需要调用
    public MyClass() {}

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

    @Override
    public void writeExternal(ObjectOutput out) throws IOException {
        // 手动指定对象的序列化过程
        out.writeObject(name);
        out.writeInt(age);
    }

    @Override
    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
        // 手动指定对象的反序列化过程
        name = (String) in.readObject();
        age = in.readInt();
    }

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }
}

public class ExternalizableExample {
    public static void main(String[] args) {
        MyClass obj = new MyClass("John", 30);

        try {
            // 将对象序列化到文件
            FileOutputStream fileOut = new FileOutputStream("object.ser");
            ObjectOutputStream out = new ObjectOutputStream(fileOut);
            out.writeObject(obj);
            out.close();
            fileOut.close();
            System.out.println("对象已被序列化到 object.ser 文件");

            // 从文件中反序列化对象
            FileInputStream fileIn = new FileInputStream("object.ser");
            ObjectInputStream in = new ObjectInputStream(fileIn);
            MyClass newObj = (MyClass) in.readObject();
            in.close();
            fileIn.close();
            System.out.println("从 object.ser 文件反序列化的对象:");
            System.out.println("Name: " + newObj.getName());
            System.out.println("Age: " + newObj.getAge());
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

在上面的示例中:

  • MyClass类实现了Externalizable接口,并提供了writeExternalreadExternal两个方法,分别用于手动指定对象的序列化和反序列化过程。
  • writeExternal方法中,我们手动将对象的字段按照特定的顺序写入到ObjectOutput对象中。
  • readExternal方法中,我们手动从ObjectInput对象中按照相同的顺序读取对象的字段。
  • 这样,我们就可以自定义对象的序列化和反序列化过程,实现更灵活的序列化机制。

透明度

通过实现Externalizable接口,开发者可以更灵活地控制对象的序列化过程,可以访问序列化的数据,并且可以在序列化和反序列化过程中进行一些自定义的操作。
总结一下,Serializable接口提供了一个简单的、自动化的序列化机制,适用于大多数情况下;而Externalizable接口提供了更灵活、更可控的序列化机制,适用于需要定制化序列化过程的情况。

序列化ID

在实现 Serializable 接口的类中,不需要显式地设置序列化ID。Java会自动生成一个序列化ID,如果没有显式指定的话。但是,建议在类中显式地声明一个 serialVersionUID,这样可以确保在类结构发生变化时,仍然能够向后兼容。

Serializable自动生成序列化ID

Java在序列化一个类时,如果没有显式地声明serialVersionUID,它会根据类的结构自动生成一个序列化ID。这个自动生成的序列化ID是基于类的结构计算得出的,因此它是与类的内容相关的。如果类的结构发生变化,比如添加或删除字段、修改字段的类型或者顺序等,那么自动生成的序列化ID也会发生变化。

Java在自动生成序列化ID时,是基于类的结构通过一定的算法计算得出的。具体而言,Java使用一个称为“Object Stream Protocol Version”(对象流协议版本)的规范,通过对类的结构进行哈希计算,生成一个64位的long型数字作为序列化ID。

这个哈希计算的过程是基于类的以下信息:

  1. 类的名称(包括包名)。
  2. 类的修饰符(public、private等)。
  3. 类的接口。
  4. 类的字段(包括字段的名称、类型、修饰符等)。
  5. 类的方法(包括方法的名称、参数列表、返回类型、修饰符等)。

通过对上述信息进行哈希计算,生成的数字就是自动生成的序列化ID。由于计算基于类的结构,因此如果类的结构发生变化,这个自动生成的序列化ID也会随之变化。

需要注意的是,这个自动生成的序列化ID并不是唯一标识一个类的最佳方式,因为它对于不同的JVM实现和编译器可能会有差异。因此,在实际应用中,建议显式声明serialVersionUID,以确保更好的序列化兼容性。手动声明serialVersionUID可以使开发者在类结构发生变化时有更多的控制权,确保向后兼容。

当类的结构发生变化时,如果原先序列化的类和反序列化时的类结构不一致,即序列化时的版本与反序列化时的版本不匹配,Java会抛出InvalidClassException异常。这是因为Java在反序列化时会检查类的序列化ID是否匹配,如果不匹配就会认为类的版本不一致,从而抛出异常。

因此,即使是使用Java自动生成的序列化ID,也需要在类的结构发生变化时进行兼容性的处理,以确保向后兼容。建议在类中显式地声明一个serialVersionUID,这样可以确保在类结构发生变化时,仍然能够向后兼容。

Externalizable不需要序列化ID

而在实现 Externalizable 接口的类中,则不需要设置 serialVersionUID,因为 Externalizable 接口不会自动生成序列化ID。相反,它要求程序员手动实现 writeExternalreadExternal 方法来控制序列化和反序列化的过程。