Java序列化

456 阅读6分钟

当我们想要将一个Java对象存储在一个临时媒介或者网络传输出去等等这些场景的时候,就需要需要使用到序列化和反序列化技术。Java序列化就是指将对象转换为字节序列的过程,而反序列化则是只将字节序列转换成目标对象的过程。

在Java中实现序列化主要通过2个接口:Serializable以及Externalizable

public interface Serializable {
}
public interface Externalizable extends java.io.Serializable {
    void writeExternal(ObjectOutput out) throws IOException;
    void readExternal(ObjectInput in) throws IOException, ClassNotFoundException;
}

我们先看看他们分别的使用方法:

Serializable序列化实现

@Data
public class Parent implements Serializable {
    public static final long serialVersionUID = 1L;
    private String name;
    private Integer age;
    private transient Integer weight; 

    public static void main(String[] args) throws IOException, ClassNotFoundException {
        Parent parent = new Parent();
        parent.setName("Tom");
        parent.setAge(30);
        parent.setWeight(120);
        // 序列化
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("./data.txt"));
        oos.writeObject(parent);
        oos.close();
        // 反序列化
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("./data.txt"));
        Parent parent1 = (Parent) ois.readObject();
        System.out.println(parent1.getName());
        System.out.println(parent1.getAge());
        System.out.println(parent1.Weight());
    }
}
/*
Tom
30
null
*/

可以看到,Serializable只是一个标记,我们可以通过ObjectOutputStreamObjectInputStream 将对象序列化到我们需要发送的媒介上去。当然使用他们的writeObject方法和readObject方法只是走的默认的序列化方法,如果我们想要定制自己的序列化方法可以在自己类中覆写这2个方法。

private void writeObject(ObjectOutputStream out) throws IOException {
    //名字后缀加CE
    out.writeObject(new StringBuffer(this.name).append("CE"));
    out.writeInt(age);
    out.writeInt(weight);
}

private void readObject(ObjectInputStream ins) throws IOException,ClassNotFoundException{
    this.name = ((StringBuffer)ins.readObject()).toString();
    this.age = ins.readInt();
    this.weight = ins.readInt();
}
/*
TomCE
30
120
*/  

这2个方法并非是什么接口的override方法,只是这个类的2个私有方法,如果我们实现了这2个方法,ObjectOutputStream和ObjectInputStream就会调用这个方法,而不是自己的默认实现!(还要注意一点,我们这些如果指定了写入和读取weight变量,那么及时变量加了transient关键字也是无效的)

Externalizable实现

@Data
public class Child implements Externalizable {
    private String name;
    private Integer age;
    @Override
    public void writeExternal(ObjectOutput out) throws IOException {
        out.writeObject(name);
        out.writeInt(age);
    }
    @Override
    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
        this.name = (String) in.readObject();
        this.age = in.readInt();
    }
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        Child child = new Child();
        child.setName("Justin");
        child.setAge(10);
        // 序列化
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("./data2.txt"));
        oos.writeObject(parent);
        oos.close();
        // 反序列化
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("./data2.txt"));
        Child child1 = (Child) ois.readObject();
        System.out.println(child1.getName());
        System.out.println(child1.getAge());
    }
}
/*
Justin
10
*/

Externalizable是继承自Serializable接口的,不过他需要我们自己去重写writeExternal()与readExternal()方法。并且实现Externalizable接口的类必须要提供一个public的无参的构造器。

他其实就是相当于对我们上面提到的自定义方式提供了一种接口方式实现。

一些序列化相关的问题

对于序列化有很多知识点,这里列举一下备忘。

serialVersionUID 作用

JAVA序列化会通过判断类的serialVersionUID来验证的版本是否一致。在进行反序列化时,JVM会把传来的字节流中的serialVersionUID于本地相应实体类的serialVersionUID进行比较。如果相同说明是一致的,可以进行反序列化,否则会出现反序列化版本一致的异常,即是InvalidCastException。

当进行拿到字节序列进行反序列化的时候,我们必须得有class文件,但是随着项目的升级,class文件也会不断的进行迭代变更。总有一天我们可能会不再支持之前类创建并序列化的对象,这时候可以修改下serialVersionUID版本号,这样原来的对象将无法序列成功。

而且我们应当指定版本号,因为如果不指定的话jvm会自动依据类的信息来生成一个id,这样即使我们的类和数据是兼容的,或者类没变,只是jvm实现不同,会导致数据无法被正确的序列化。所以建议我们对于所有需要序列化的类都加上该变量。

想要序列化为什么要实现Serializable?

之所以需要用Serializable来标记可以被序列化的类,其目的其实就是为了让一些类不能够被序列化!我们当然可以让Java所有的类默认都支持序列化,但是其实序列化是有风险的,比如《Effective java》中描述了一系列实现Serializable的弊端:

  1. 一旦一个类被发布,就大大降低了「改变这个类的实现」的灵活性。若一个类实现了Serializable接口,它就成了这个类导出API的一部分。
  2. 增加了出现Bug和安全漏洞的可能性。序列化机制是一种语言之外的对象创建机制,反序列化是一个「隐藏的构造器」,具备与其他构造器相同的特点。因此,反序列化过程必须要保证所有的约束关系。
  3. 随着发行新的版本,相关的测试负担也增加了。
  4. 序列化对象时,不仅会序列化当前对象本身,还会对该对象引用的其他对象也进行序列化。如果一个对象包含的成员变量是容器类等并深层引用时(对象是链表形式),此时序列化开销会很大,这时必须要采用其他一些手段处理。

所以Java其实本质想的是让所有的类都不能实现序列化,(因为比如Socket,Thread这些类如果可以被序列化进行传输或保存,即使他们被反序列化了,我们也无法对其分配资源的)只有当我们明确需要这个类需要被序列化的时候,我们才加上标注,这样JVM才放行(ObjectXputStream的readObject和writeObject方法会检测类是否instanceof Serializable)。

静态变量会不会被序列化?

不会,他不属于对象,属于类。

同一对象序列化多次,会将这个对象序列化多次吗?

不会,一个对象序列化的时候,写入多次,都还只是一个对象。当反序列化后,反序列化出来的对象都是同一个对象。比如

oos.writeObject(o1);
oos.writeObject(o2);
(o1 = ooi.readObject()) == (o2 = ooi.readObject())

当序列化对象的时候,每个对象都有一个编号,当再次序列化的时候,会检查对象是否被序列化过,如果有则只输出编号,而不再序列该对象。

Transient关键字

指定某个属性不被序列化,并不保证一定有效,比如上文中自定义序列化的方法,即使指定了也可以序列化和反序列化。

一些序列化注意事项

  • 如果一个类的属性是一个对象类的话,那么这个对象类也必须实现Serializable接口,否则序列化失败。
  • 子类实现了 Serializable 接口,父类没有实现 Serializable 接口,父类不会被序列化,即反序列化后父类的属性就丢失了。
  • 父类实现了Serializable接口,子类不需要实现。