序列化1

131 阅读9分钟

序列化

序列化和反序列化

序列化将对象写入到IO流中,是通过某种算法将存储于内存中的对象转换成可以用于持久化存储或者通信的形式的过程。

反序列化从IO流中恢复对象,是将这种被持久化存储或者通信的数据通过对应解析算法还原成对象的过程,它是序列化的逆向操作。

为什么需要序列化?

序列化机制允许将实现序列化的Java对象转换位字节序列,这些字节序列可以保存在磁盘上,或通过网络传输,以达到以后恢复成原来的对象。序列化机制使得对象可以脱离程序的运行而独立存在。

序列化的场景

所有可在网络上传输的对象都必须是可序列化的

1、RPC调用中对象在网络中的传输。

2、内存中的java对象持久化到磁盘中。

3、数据交互。

数据的持久化

数据交互

前端请求后端接口数据的时候,后端需要返回 JSON 数据,这就是后端将 Java 堆中的对象序列化为了 JSON 数据传给前端,前端可以根据自身需求直接使用或者将其反序列化为 JS 对象

RPC

RPC 远程调用过程中,调用者和被调用者必须约定好序列化和反序列化算法,比如 A 应用将 User 对象序列化为了 JSON 数据传给 B 应用,User 对象数据为 {"id": 1, "name": "long"},到达 B 应用的时候需要将这些数据反序列化为对象,如果此时 B 应用的反序列化算法是 XML 的话那么肯定就解析失败了,所以必须都得约定好他们都采用 JSON 序列化算法,那么基于 JSON 标准就能成功解析出 User 对象

java中序列化

序列化规范

1、实现java.io.Serializable接口。
Serializable接口是一个标记接口,不用实现任何方法。一旦实现了此接口,该类的对象就是可序列化的。 2、transient关键字修饰。 某些属性不需要序列化。使用transient关键字选择不需要序列化的字段。 使用transient修饰的属性,java序列化时,会忽略掉此字段,所以反序列化出的对象,被transient修饰的属性是默认值。

  • 基本类型:0
  • boolean类型:false
  • 引用类型:null 3、可选的自定义序列化
  • 控制序列化的方式,需要选择那些属性进行序列化。
  • 对序列化数据进行编码加密。 4、Externalizable 彻底的自定义序列化,自定义存储信息 实现Externalizable接口。
//在writeObject()执行前执行
private Object writeReplace() throws ObjectStreamException {}
//在readObject()执行后执行
private Object readResolve() throws ObjectStreamException {}

writeReplace:将任意对象代替目标序列化对象。
readResolve:序列化时替换反序列化出的对象,反序列化出来的对象被立即丢弃

Serializable

序列化

步骤一:创建一个ObjectOutputStream输出流;
步骤二:调用ObjectOutputStream对象的writeObject输出可序列化对象。

###反序列化 步骤一:创建一个ObjectInputStream输入流;
步骤二:调用ObjectInputStream对象的readObject()得到序列化的对象。

1、反序列化并不会调用构造方法,反序列化的对象是jvm自己生成的对象,不通能过构造方法生成。
2、如果一个可序列化的类的成员不是基本类型,也不是String类型,那这个引用类型也必须是可序列化的;否则,会导致此类不能序列化。
3、序列恶化的和反序列化顺序是一致的。 3、同一对象序列化多次的机制。
1、同一对象不会学序列化多次,即一个对象序列化两次,反序列化出的对象是相同的。(引用相同即对象相同。)

java序列化算法

java序列化算法

1、所有保存到磁盘的对象都有一个序列化编码号。
2、当程序试图序列化一个对象时,会先检查此对象是否已经序列化过,只有此对象从未(在此虚拟机)被序列化过,才会将此对象序列化为字节序列输出。 3、如果此对象已经序列化过,则直接输出编号即可。
image.png

java序列化的问题

由于java序利化算法不会重复序列化同一个对象,只会记录已序列化对象的编号。如果序列化一个可变对象(对象内的内容可更改)后,更改了对象内容,再次序列化,并不会再次将此对象转换为字节序列,而只是保存序列化编号。

public static void main(String[] args) throws Exception {
    //序列化
    String path = SysConstant.SERIAL_PATH + "E_transient.txt";
    try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(path));
            ObjectInputStream ios = new ObjectInputStream(new FileInputStream(path))) {
        Person person = new Person("9龙", 23);
        person.setHeight(185);
        System.out.println(person);
        oos.writeObject(person);
        //修改name
        Person p1 = (Person) ios.readObject();
        System.out.println(p1);
        //Person{name='9龙', age=23', singlehood=true', height=185cm}
        //Person{name='null', age=0', singlehood=false', height=185cm}
    } catch (Exception e) {
        e.printStackTrace();
    }
}

可选的自定义序列化

transient

有些时候,我们有这样的需求,某些属性不需要序列化。使用transient关键字选择不需要序列化的字段。'

public static void main(String[] args) throws Exception {
    //序列化
    String path = SysConstant.SERIAL_PATH + "E_transient.txt";
    try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(path));
            ObjectInputStream ios = new ObjectInputStream(new FileInputStream(path))) {
        Person person = new Person("9龙", 23);
        person.setHeight(185);
        System.out.println(person);
        oos.writeObject(person);
        //修改name
        Person p1 = (Person) ios.readObject();
        System.out.println(p1);
        //Person{name='9龙', age=23', singlehood=true', height=185cm}
        //Person{name='null', age=0', singlehood=false', height=185cm}
    } catch (Exception e) {
        e.printStackTrace();
    }
}

从输出我们看到,使用transient修饰的属性,java序列化时,会忽略掉此字段,所以反序列化出的对象,被transient修饰的属性是默认值。对于引用类型,值是null;基本类型,值是0;boolean类型,值是false。

可选的自定义序列化

使用transient虽然简单,但将此属性完全隔离在了序列化之外。java提供了可选的自定义序列化。 可以进行控制序列化的方式,或者对序列化数据进行编码加密等。
通过重写writeObject与readObject方法:

#序列化
private void writeObject(java.io.ObjectOutputStream out) throws IOException;
#反序列化
private void readObject(java.io.ObjectIutputStream in) throws IOException,ClassNotFoundException;
#初始化对象,如果序列化流不存在。
private void readObjectNoData() throws ObjectStreamException;

反转加密

static class Person implements Serializable {

    private String name;
    
    private int age;
    
    private void writeObject(ObjectOutputStream out) throws IOException {
        //将名字反转写入二进制流
        out.writeObject(new StringBuffer(this.name).reverse());
        out.writeInt(age);
    }
    
    private void readObject(ObjectInputStream ins) throws IOException, ClassNotFoundException {
        //将读出的字符串反转恢复回来
        this.name = ((StringBuffer) ins.readObject()).reverse().toString();
        this.age = ins.readInt();
    }
}

当序列化流不完整时,readObjectNoData()方法可以用来正确地初始化反序列化的对象。例如,使用不同类接收反序列化对象,或者序列化流被篡改时,系统都会调用readObjectNoData()方法来初始化反序列化的对象。

彻底的自定义序列化

writeReplace

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

static class Person implements Serializable {
    
    private String name;
    
    private int age;
    
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
    
    private Object writeReplace() throws ObjectStreamException {
        ArrayList<Object> list = new ArrayList<>(2);
        list.add(this.name);
        list.add(this.age);
        return list;
    }
}

readResolve

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

static class Person implements Serializable {

    private String name;

    private int age;

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

            private Object readResolve() throws ObjectStreamException {
         HashMap<String, Integer> map = new HashMap<>();
         map.put(name,age);
        return map;
    }
}

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

Externalizable:强制自定义序列化

通过实现Externalizable接口,必须实现writeExternal、readExternal方法。

public interface Externalizable extends java.io.Serializable {
   void writeExternal(ObjectOutput out) throws IOException;
   void readExternal(ObjectInput in) throws IOException, ClassNotFoundException;
}
@Data
static class ExPerson implements Externalizable {
    
    private String name;
    
    private int age;
    
    //注意,必须加上pulic 无参构造器
    public ExPerson() {
    }
    
    public ExPerson(String name, int age) {
        this.name = name;
        this.age = age;
    }
    
    @Override
    public void writeExternal(ObjectOutput out) throws IOException {
        //将name反转后写入二进制流
        StringBuffer reverse = new StringBuffer(name).reverse();
        System.out.println(reverse.toString());
        out.writeObject(reverse);
        out.writeInt(age);
        
    }
    
    @Override
    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
        //将读取的字符串反转后赋值给name实例变量
        this.name = ((StringBuffer) in.readObject()).reverse().toString();
        System.out.println(name);
        this.age = in.readInt();
    }
}

注意:Externalizable接口不同于Serializable接口,实现此接口必须实现接口中的两个方法实现自定义序列化,这是强制性的;特别之处是必须提供pulic的无参构造器,因为在反序列化的时候需要反射创建对象。

两种序列化对比

实现Serializable接口实现Externalizable接口
系统自动存储必要的信息程序员决定存储哪些信息
Java内建支持,易于实现,只需要实现该接口即可,无需任何代码支持必须实现接口内的两个方法
性能略差性能略好

虽然Externalizable接口带来了一定的性能提升,但变成复杂度也提高了,所以一般通过实现Serializable接口进行序列化。

序列化版本号serialVersionUID

##序列化版本号 我们知道,反序列化必须拥有class文件,但随着项目的升级,class文件也会升级,序列化怎么保证升级前后的兼容性呢? java序列化提供了一个private static final long serialVersionUID 的序列化版本号,只有版本号相同,即使更改了序列化属性,对象也可以正确被反序列化回来。

static class Person implements Serializable {
    private static final long serialVersionUID = 1111013L;
    private String name;
    private int age;
}

如果反序列化使用的class的版本号与序列化时使用的不一致,反序列化会报InvalidClassException异常。 image.png 序列化版本号可自由指定,如果不指定,JVM会根据类信息自己计算一个版本号,这样随着class的升级,就无法正确反序列化;不指定版本号另一个明显隐患是,不利于jvm间的移植,可能class文件没有更改,但不同jvm可能计算的规则不一样,这样也会导致无法反序列化。

  • 如果只是修改了方法,反序列化不容影响,则无需修改版本号;

  • 如果只是修改了静态变量,瞬态变量(transient修饰的变量),反序列化不受影响,无需修改版本号;

  • 如果修改了非瞬态变量,则可能导致反序列化失败。如果新类中实例变量的类型与序列化时类的类型不一致,则会反序列化失败,这时候需要更改serialVersionUID。 如果只是新增了实例变量,则反序列化回来新增的是默认值;如果减少了实例变量,反序列化时会忽略掉减少的实例变量。