前言

  • 什么是序列化和反序列化

    • Serializatable(序列化):将 Java 对象转化为字节序列进行传输/保存的过程。
    • DeSerialization(反序列化):将字节序列转化为 Java 对象。
  • 意义

    • 序列化机制允许将实现序列化机制的Java 对象准换回字节序列,这些字节序列可以保存到磁盘上,也通过通过网络传输后,再转换回原来的对象。
    • 该机制使得对象可以脱离程序运行而独立存在。
  • 常见用途

    1. 将对象的字节序列保存到磁盘上。 建议:程序里的每个 JavaBean 对象都实现 Serializable 接口
    2. 在网络通信中传输对象的字节序列。

实现

Java 中,对象如果想要实现序列化,必须要实现以下两个接口之一:

  • Serializable
  • Externalizable

Serializable

Serializable 的序列化和反序列化大致有以下4种,后3种可以提供不同程度(越来越灵活)的序列化。

  1. 通过 ObjectOutputStream 和 ObjectInputStream 默认的序列化和反序列化方式
  2. 使用 transient 关键字,可选序列化的字段。
  3. Serializable 序列化类定义了 writeObject(ObjectOutputStream out) 和 readObject(ObjectInputStream in),则通过类定义的方法进行操作。
    • private void writeObject(java.io.ObjectOutputStream out) throws IOException;
    • private void readObject(java.io.ObjectIutputStream in) throws IOException,ClassNotFoundException;
    • private void readObjectNoData() throws ObjectStreamException;
  4. Serializable 序列化类定义了 writeReplace 和 readResolve 方法。
    • ANY-ACCESS-MODIFIER Object writeReplace() throws ObjectStreamException;
    • ANY-ACCESS-MODIFIER Object readResolve() throws ObjectStreamException;

默认方式

// Test Class 

@Slf4j
@Data
class SerializationClazz implements Serializable {
    //    private static String staticVar = "123";
    private static final Long serialVersionUID = 1L;
    /**
     * 名称
     */
    private String name;
    /**
     * 年龄
     */
    private Integer age;
    /**
     * 性别
     */
    private int sex;

    private final String fff = "1";

    private void writeObject(ObjectOutputStream out) throws IOException {
        log.info("writeObject");
        //将名字反转写入二进制流
        out.writeObject(new StringBuffer(this.name).reverse().toString());
        out.writeInt(age);
    }

    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
        log.info("readObject");
        this.name = (String) in.readObject();
        this.age = in.readInt();
    }


    /**
     * 构造器
     */
    public SerializationClazz(String name) {
        log.info("SerializationClazz Construct:name={}", name);
        this.name = name;
    }
    public SerializationClazz(String name, Integer age) {
        log.info("SerializationClazz Construct:name={},age={}", name, age);
        this.name = name;
        this.age = age;
    }
    public SerializationClazz(String name, Integer age, int sex) {
        log.info("SerializationClazz Construct:name={},age={},sex={}", name, age, sex);
        this.name = name;
        this.age = age;
        this.sex = sex;
    }
}

// Test Function
@Test
public void SerializationTest() {
    SerializationClazz serializationClazz = new SerializationClazz("Serialization", 20);
    try {
        // 序列化
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("out.txt"));
        objectOutputStream.writeObject(serializationClazz);
        objectOutputStream.close();
        
		// 反序列化
        ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("out.txt"));
        SerializationClazz newClazz = (SerializationClazz) objectInputStream.readObject();
        log.info("反序列化后对象:{}", newClazz);
        objectInputStream.close();
    } catch (IOException e) {
        e.printStackTrace();
    } catch (ClassNotFoundException exception) {
        exception.printStackTrace();
    }
}

我们可以看到:

  • 这个对象的所有属性(包括private和引用的属性),除了static和final修饰的,都可以被序列化和反序列化。

transient 关键字

使用transient关键字选择不需要序列化的字段。

// Test Class 

@Slf4j
@Data
class SerializationClazz implements Serializable {
    //    private static String staticVar = "123";
    private static final Long serialVersionUID = 1L;
    /**
     * 名称
     */
    private String name;
    /**
     * 年龄
     */
    private Integer age;
    /**
     * 性别
     */
    private transient int sex;

    private void writeObject(ObjectOutputStream out) throws IOException {
        log.info("writeObject");
        //将名字反转写入二进制流
        out.writeObject(new StringBuffer(this.name).reverse().toString());
        out.writeInt(age);
    }

    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
        log.info("readObject");
        this.name = (String) in.readObject();
        this.age = in.readInt();
    }


    /**
     * 构造器
     */
    public SerializationClazz(String name) {
        log.info("SerializationClazz Construct:name={}", name);
        this.name = name;
    }
    public SerializationClazz(String name, Integer age) {
        log.info("SerializationClazz Construct:name={},age={}", name, age);
        this.name = name;
        this.age = age;
    }
    public SerializationClazz(String name, Integer age, int sex) {
        log.info("SerializationClazz Construct:name={},age={},sex={}", name, age, sex);
        this.name = name;
        this.age = age;
        this.sex = sex;
    }
}

// Test Function
@Test
public void SerializationTest() {
    SerializationClazz serializationClazz = new SerializationClazz("Serialization", 20);
    try {
        // 序列化
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("out.txt"));
        objectOutputStream.writeObject(serializationClazz);
        objectOutputStream.close();
        
		// 反序列化
        ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("out.txt"));
        SerializationClazz newClazz = (SerializationClazz) objectInputStream.readObject();
        log.info("反序列化后对象:{}", newClazz);
        objectInputStream.close();
    } catch (IOException e) {
        e.printStackTrace();
    } catch (ClassNotFoundException exception) {
        exception.printStackTrace();
    }
}

从输出结果,我们可以看出transient修饰的sex属性,Java 序列化的时候,会忽略该字段属性。 对于引用类型,值是null;基本类型,值是0;boolean类型,值是false。

使用 transient 关键字,我们是能很方便的忽略某些字段,但是有时候我们希望进行编解码或者加密时,transient 的灵活度并不能满足我们的要求。

序列化类重写 writeObject 和 readObject 方法


/**
 * Test writeObject and readObject
 */
@Slf4j
@Data
class SerializationClazz2 implements Serializable {
    //    private static String staticVar = "123";
    private static final Long serialVersionUID = 1L;
    /**
     * 名称
     */
    private String name;
    /**
     * 年龄
     */
    private Integer age;
    /**
     * 性别
     */
    private int sex;

    private void writeObject(ObjectOutputStream out) throws IOException {
        log.info("writeObject");
        out.writeObject(new StringBuilder(this.name).reverse());
        out.writeInt(age);
    }

    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
        log.info("readObject");
        this.name = ((StringBuilder) in.readObject()).reverse().toString();
        this.age = in.readInt();
    }

    /**
     * 构造器
     */
    public SerializationClazz2(String name) {
        log.info("SerializationClazz Construct:name={}", name);
        this.name = name;
    }

    public SerializationClazz2(String name, Integer age) {
        log.info("SerializationClazz Construct:name={},age={}", name, age);
        this.name = name;
        this.age = age;
    }

    public SerializationClazz2(String name, Integer age, int sex) {
        log.info("SerializationClazz Construct:name={},age={},sex={}", name, age, sex);
        this.name = name;
        this.age = age;
        this.sex = sex;
    }
}


// Test Function
@Test
public void SerializationTest2() {
    SerializationClazz2 serializationClazz = new SerializationClazz2("Serialization", 20, 1);
    try {
        //序列化
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("out.txt"));
        objectOutputStream.writeObject(serializationClazz);
        objectOutputStream.close();

        // 反序列化
        ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("out.txt"));
        SerializationClazz2 newClazz = (SerializationClazz2) objectInputStream.readObject();
        log.info("反序列化后对象:{}", newClazz);
        objectInputStream.close();
    } catch (IOException e) {
        e.printStackTrace();
    } catch (ClassNotFoundException exception) {
        exception.printStackTrace();
    }
}

  • 通过 ObjectOutputStream 会调用对象的 writeObject 方法,并将要序列化的属性写入。
  • 通过 InputOutputStream 会调用对象的 readObject 方法,将序列化值读取出来。需要注意的是这里需要按照写入的顺序进行读取,并且写入的规则是什么样子的,需要按写入规则反转进行读取。
  • 我们没有序列化的属性,会在反序列化后填入类型的默认值。

此外,关于 readObjectNoData 方法,我们可以看一下他的使用场景

  1. Serializable对象反序列化时,由于序列化与反序列化提供的类版本不同,序列化的class的super class不同于序列化时的class的super class;
  2. 序列化流被篡改时,系统都会调用readObjectNoData()方法来初始化反序列化的对象。

如果发生上述场景的时候,没有定义readObjectNoData,那么就会初始化成类型的默认值;如果这时定义 readObjectNoData,那么 readObjectNoData 会替代 readObject 方法生效。

writeReplace 和 readResolve 方法

writeReplace

在序列化时,会先调用此方法,再调用writeObject方法。此方法可将任意对象代替目标序列化对象


/**
 * Test writeReplace
 */
@Slf4j
@Data
class SerializationClazz3 implements Serializable {
    //    private static String staticVar = "123";
    private static final Long serialVersionUID = 1L;
    /**
     * 名称
     */
    private String name;
    /**
     * 年龄
     */
    private Integer age;
    /**
     * 性别
     */
    private int sex;

    private Object writeReplace() throws IOException {
        log.info("writeReplace");
        return new SerializationClazz3("writeReplace", 2, 2);
    }

    private void writeObject(ObjectOutputStream out) throws IOException {
        log.info("writeObject");
        out.writeObject(this.name);
        out.writeInt(age);
    }

    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
        log.info("readObject");
        this.name = in.readObject().toString();
        this.age = in.readInt();
    }

    /**
     * 构造器
     */
    public SerializationClazz3(String name) {
        log.info("SerializationClazz3 Construct:name={}", name);
        this.name = name;
    }

    public SerializationClazz3(String name, Integer age) {
        log.info("SerializationClazz3 Construct:name={},age={}", name, age);
        this.name = name;
        this.age = age;
    }

    public SerializationClazz3(String name, Integer age, int sex) {
        log.info("SerializationClazz3 Construct:name={},age={},sex={}", name, age, sex);
        this.name = name;
        this.age = age;
        this.sex = sex;
    }
}


// Test Func
@Test
public void SerializationTest3() {
    SerializationClazz3 serializationClazz = new SerializationClazz3("Serialization", 222, 1);
    try {
        //序列化
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("out.txt"));
        objectOutputStream.writeObject(serializationClazz);
        objectOutputStream.close();

        // 反序列化
        ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("out.txt"));
        SerializationClazz3 newClazz = (SerializationClazz3) objectInputStream.readObject();
        log.info("反序列化后对象:{}", newClazz);
        objectInputStream.close();
    } catch (IOException e) {
        e.printStackTrace();
    } catch (ClassNotFoundException exception) {
        exception.printStackTrace();
    }
}

输出结果:

SerializationClazz3 - SerializationClazz3 Construct:name=Serialization,age=222,sex=1
SerializationClazz3 - writeReplace
SerializationClazz3 - SerializationClazz3 Construct:name=writeReplace,age=2,sex=2
SerializationClazz3 - writeObject
SerializationClazz3 - readObject
SerializationTest - 反序列化后对象:SerializationClazz3(name=writeReplace, age=2, sex=0)
  1. writeReplace先于writeObject执行
  2. writeReplace返回的对象可以是任意类型

readResolve

反序列化时替换反序列化出的对象,反序列化出来的对象被立即丢弃。此方法在readeObject后调用。



/**
 * Test readResolve
 */
@Slf4j
@Data
class SerializationClazz4 implements Serializable {
    //    private static String staticVar = "123";
    private static final Long serialVersionUID = 1L;
    /**
     * 名称
     */
    private String name;
    /**
     * 年龄
     */
    private Integer age;
    /**
     * 性别
     */
    private int sex;
    
    private Object readResolve() throws IOException {
        log.info("readResolve");
        return new SerializationClazz4("readResolve", 1, 1);
    }

    /**
     * 构造器
     */
    public SerializationClazz4(String name) {
        log.info("SerializationClazz4 Construct:name={}", name);
        this.name = name;
    }

    public SerializationClazz4(String name, Integer age) {
        log.info("SerializationClazz4 Construct:name={},age={}", name, age);
        this.name = name;
        this.age = age;
    }

    public SerializationClazz4(String name, Integer age, int sex) {
        log.info("SerializationClazz4 Construct:name={},age={},sex={}", name, age, sex);
        this.name = name;
        this.age = age;
        this.sex = sex;
    }
}



// Test Func
@Test
public void SerializationTest4() {
    SerializationClazz4 serializationClazz = new SerializationClazz4("Serialization", 222, 1);
    try {
        //序列化
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("out.txt"));
        objectOutputStream.writeObject(serializationClazz);
        objectOutputStream.close();

        // 反序列化
        ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("out.txt"));
        SerializationClazz4 newClazz = (SerializationClazz4) objectInputStream.readObject();
        log.info("反序列化后对象:{}", newClazz);
        objectInputStream.close();
    } catch (IOException e) {
        e.printStackTrace();
    } catch (ClassNotFoundException exception) {
        exception.printStackTrace();
    }
}
  1. 执行顺序readObject 先于 readResolve执行
  2. readResolve常用来反序列单例类,保证单例类的唯一性。(详见)

注意:

  • readResolve与writeReplace的访问修饰符可以是private、protected、public,如果父类重写了这两个方法,子类都需要根据自身需求重写,这显然不是一个好的设计。
  • 通常建议对于final修饰的类重写readResolve方法没有问题;否则,重写readResolve使用private修饰。

Externalizable

Externalizable 继承自 Serializable使用 Externalizable 接口需要实现 writeExternal 以及 readExternal 方法。

测试类:


@Slf4j
@Data
class ExternalizationClazz implements Externalizable {
    /**
     * 名称
     */
    private String name;
    /**
     * 年龄
     */
    private Integer age;

    public void writeExternal(ObjectOutput out) throws IOException {
        log.info("writeExternal");
        out.writeObject(this.name + "writeExternal");
        out.writeInt(age);
    }

    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
        log.info("readExternal");
        this.name = in.readObject().toString() + "readExternal";
        this.age = in.readInt();
    }

    private Object writeReplace() throws IOException {
        log.info("writeReplace");
        return new ExternalizationClazz("writeReplace", 2);
    }

    private void writeObject(ObjectOutputStream out) throws IOException {
        log.info("writeObject");
        out.writeObject(this.name + "writeObject");
        out.writeInt(age);
    }


    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
        log.info("readObject");
        this.name = in.readObject().toString() + "readObject";
        this.age = in.readInt();
    }


    private Object readResolve() throws IOException {
        log.info("readResolve");
        return new ExternalizationClazz("readResolve", 9);
    }


    /**
     * 构造器
     */
    public ExternalizationClazz(){

    }
    public ExternalizationClazz(String name, Integer age) {
        log.info("ExternalizationClazz Construct:name={},age={}", name, age);
        this.name = name;
        this.age = age;
    }

}

测试方法:

@Test
public void test() {
    ExternalizationClazz clazz = new ExternalizationClazz("Test", 11);
    try {
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("out.txt"));
        objectOutputStream.writeObject(clazz);
        objectOutputStream.flush();
        objectOutputStream.close();

        ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("out.txt"));
        ExternalizationClazz newClazz = (ExternalizationClazz) objectInputStream.readObject();
        objectInputStream.close();

        log.info("clazz => {}", clazz);
        log.info("newClazz => {}", newClazz);
    } catch (IOException e) {
        e.printStackTrace();
    } catch (ClassNotFoundException e) {
        e.printStackTrace();
    }
}

输出结果

ExternalizationClazz - ExternalizationClazz Construct:name=Test,age=11
ExternalizationClazz - writeReplace
ExternalizationClazz - ExternalizationClazz Construct:name=writeReplace,age=2
ExternalizationClazz - writeExternal
ExternalizationClazz - readExternal
ExternalizationClazz - readResolve
ExternalizationClazz - ExternalizationClazz Construct:name=readResolve,age=9
ExternalizationTest - clazz => ExternalizationClazz(name=Test, age=11)
ExternalizationTest - newClazz => ExternalizationClazz(name=readResolve, age=9)

可以看出

  1. writeObjectreadReadObjectSerializable 接口继承后独有的方法。因此在 Externalizable实现后不起作用,具体可看 readOrdinaryObject 源码

    // 如果类描述符实现了 Externalizable 接口,则调用 readExternalData 进行读取数据。
    if (desc.isExternalizable()) {
        // 调用 readExternal 方法
        readExternalData((Externalizable) obj, desc);
    } else {
        // 调用 readObject 方法
        readSerialData(obj, desc);
    }
    
  2. Externalizable 接口的实现方式一定要有默认的无参构造函数。否则会抛出 no valid constructor 异常。

Serializable VS Externalizable

  1. Serializable 是标识接口;Externalizable 继承了Serializable 接口,实现该接口需重写 readExternal 和 writerExternal 方法。

    public interface Serializable {
    }
    
    
    public interface Externalizable extends java.io.Serializable {
        void writeExternal(ObjectOutput out) throws IOException;
        void readExternal(ObjectInput in) throws IOException, ClassNotFoundException;   
    }
    
  2. Serializable 提供多种方式序列化:

    1. 默认序列化,transient 关键字忽略某些属性序列化
    2. readObjectwriteObject 方法自定义序列化

    Externalizable只提供了 readExternalwriteExternal 方法序列化

    当然还有公共的都能使用的自定义序列化方法 writeReplacereadResolve 方法。

  3. Serializable 无需无参构造法方法;而 Externalizable 需要提供无参构造方法,否则将抛出 no valid contructor 异常

  4. Externalizable 不需要产生序列化ID( serialVersionUID),而 Serializable 需要产生序列化版本( serialVersionUID)。这与他们的序列化方式有关。

  5. 相比较 SerializableExternalizable 序列化、反序列化速度更快,内存更低。

  6. 大多数场景下推荐使用 Serializable ,而且 Serializable 提供的默认序列方式,可以简化序列化和反序列化操作。而 Externalizable 适用于需要完全控制序列化和反序列化的地方,以及关注性能和效率的地方。