Loading

Java序列化



什么是序列化?


一句话概括。序列化将对象的状态信息转换为可以存储或传输的形式的过程。而Java序列化,就是指把Java对象转换为“有序”字节序列的过程。相反的,把“有序”字节序列恢复为Java对象的过程称之为反序列化

怎么理解上面的描述呢?从序列化的定义的可以看出,序列化其实是一个用来保障存储和传输的机制,而其针对的对象,肯定就是那些不便存储和传输的信息,而这体现在java编程中,就是类的实例 —— 类对象。

上面提到的“有序”,它并不是真的有顺序,其实是比较抽象化的表达,以此来表达经过序列化处理的对象,能够通过反序列化恢复成原来的样子这样一个过程

举个例子,你要搬家了,家里有个大鞋架,因为鞋架太大了,不便存储和运输,于是你把鞋机拆散了,然后把拆解步骤记录下来,运输到新家后,你再根据步骤记录把零件组装成鞋架原来的样子。拆鞋架和组装鞋架其实就是序列化和反序列的过程,而你记录的那份拆解步骤,就是序列化协议。


怎么实现序列化?



一、实现Serializable接口

定义一个类实现Serializable接口:

public class Rectangle implements Serializable {

    private int width;
    private int length;

    public Rectangle(){}

    public Rectangle(int width,int length){
        this.width = width;
        this.length = length;
    }

    public int getWidth() {
        return width;
    }

    public void setWidth(int width) {
        this.width = width;
    }

    public int getLength() {
        return length;
    }

    public void setLength(int length) {
        this.length = length;
    }

    @Override
    public String toString() {
        return "Rectangle{" +
                "width=" + width +
                ", length=" + length +
                '}';
    }

}

二、实现Externalizable接口

实现writeExternalreadExternal方法

public class Rectangle implements Externalizable {

    private int width;
    private int length;

    public Rectangle(){}

    public Rectangle(int width,int length){
        this.width = width;
        this.length = length;
    }

    public int getWidth() {
        return width;
    }

    public void setWidth(int width) {
        this.width = width;
    }

    public int getLength() {
        return length;
    }

    public void setLength(int length) {
        this.length = length;
    }

    @Override
    public String toString() {
        return "Rectangle{" +
                "width=" + width +
                ", length=" + length +
                '}';
    }

    @Override
    public void writeExternal(ObjectOutput out) throws IOException {
        
    }

    @Override
    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {

    }
}

序列化和反序列化

public static void main(String[] args){

    /* 序列化 */
    Rectangle rectangle = new Rectangle(6,8);
    try(ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("rectangle.out"))){
        oos.writeObject(rectangle);
    }catch(Exception ex){
        ex.printStackTrace();
    }

    /* 反序列化 */
    try(ObjectInputStream ois = new ObjectInputStream(new FileInputStream("rectangle.out"))){
        Rectangle rectangle1 = (Rectangle) ois.readObject();
        System.out.println(rectangle1);
    }catch(Exception ex){
        ex.printStackTrace();
    }

}

正常的输出结果:

Rectangle{width=6, length=8}

当实现Externalizable接口,没有对writeExternalreadExternal重写时,结果如下

Rectangle{width=0, length=0}

也就是说的,实现Externalizable接口,需要自己实现序列化和反序列逻辑,编程人员用比较高的灵活度,这点在接下来的自定义序列化会讲到


自定义序列化?



transient关键字

transient关键字修饰的成员的变量,序列化时将被忽略。该方法决定变量是否序列化

该自定义方法级别最低,当使用的下面的任一方法时,transient关键字将失效。换句话说,使用下面各方法进行序列化自定义,那么transient修饰的变量同样会被序列化。


自定义readObject和writeObject方法

通过的类中自定义readObjectwriteObject方法,可以控制该类的序列化和反序列化逻辑,jvm在进行序列化的时候会自动调用readObject方法,反序列化调用writeObject

private void readObject(ObjectInputStream ois) throws IOException { 
    this.length = ois.readInt() / 100;
    this.width = ois.readInt();
}

private void writeObject(ObjectOutputStream oos)throws IOException{
    this.length = this.length * 100;
    oos.writeInt(this.length);
    oos.writeInt(this.width);
}

自定义writeReplace方法

writeReplace方法没有入参,用于改变序列化对象的类型,且会使用默认的序列化机。也就是说,上面提到的自定义readObject和writeObject方法都将失效。以下为实例:

private Object writeReplace() throws ObjectStreamException {

    List<Object> list = new ArrayList<>();
    list.add(this.length * 1000);
    list.add(this.width *1000);
    return list; 
}

相应的,在进行反序列化的时候,就应该使用新的类型进行接收,如该例子,应该用List<Object> 接收反序列化对象。


自定义readResolve方法

readResolve方法用于替换反序列化出来的对象,该方法同样没有入参。

private Object readResolve() throws ObjectStreamException {

    System.out.println("自定义readResolve方法被调用");
    return new Rectangle(9,10);
}

由于没发拿到序列化的信息,因此常用来控制单例模式下,反序列化出来的对象为原先的单例对象,以维护单例模式中反序列化对象的单一性。


实现Externalizable接口

这个已经在上面实例化方式中有所提及。实现该接口必须强制实现writeExternalreadExternal 两个方法如下:

@Override
public void writeExternal(ObjectOutput out) throws IOException {
    this.length = this.length * 100;
    out.writeInt(this.length);
    out.writeInt(this.width);
}

@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
    this.length = in.readInt() / 100;
    this.width = in.readInt();
}

该效果等同于上面实现Serializable接口,然后自定义readObjectwriteObjectf方法


序列化的注意事项


  1. serialVersionUID 序列化版本号

    用来区分需要序列化类的版本。假如序列化的类中发生了变量的修改,那么该版本号一般也要跟着修改,才能够在反序列化的时候得到对应版本的类对象,否则会抛出InvalidClassException异常

  2. 序列化与类的初始化一样,是的一个递归的过程。

    当一个类实现了序列化接口,该类的成员除了基本类型和String类型之外,其他的引用类型也必须是可序列化的;否则会抛NotSerializableException异常

  3. 同一对象多次序列化只会序列化一次

    如果程序对同一个对象有多次序列化,那么只会在第一次进行序列化,后续实际上是返回一个编号进行区分

  4. 反序列化的顺序与序列化时的顺序一致,类似队列模型


序列化的使用场景


讲了这么多,那序列化到底用在什么地方呢?

由jvm的内存结构相关知识我们可以知道,java对象都被保存在堆内存中。在jvm处于运行状态的时候,我们能够对对象的进行复用。但一旦jvm生命周期结束,相关对象也随之被回收。也就是说的,如果希望在jvm停止后还能够拿到某些对象,这时候就需要用到java的序列化。

因此大概由以下几种场景会使用到java的序列化

  • 当你想把的内存中的对象状态保存到一个文件中或者数据库中时候;
  • 当你想用套接字在网络上传送对象的时候;
  • 当你想通过RMI(远程方法调用)传输对象的时候;
posted @ 2021-08-04 14:09  Acelin_H  阅读(519)  评论(0编辑  收藏  举报