序列化——谨慎实现Serializable

何为序列化

序列化与反序列化对应。

序列化:将一个对象转为字节流

反序列化: 将字节流转为对象

通过序列化继续,可以将对象编码之后,通过网络传输到其他应用中,再反序列化,实现远程交互调用。


如何序列化

要想使一个类的实例可以实例化,只需要实现 Serializable 即可。

正因为实现序列化太简单,很容易给程序员一种误解:序列化很容易,不需要考虑太多。

实际的情形却比较复杂。

标记类可实例化很简单,执行实例化动作的直接开销也很低。


序列化实例

我们先创建一个Car类

@Data
@Accessors(chain = true)
public class Car implements Serializable {
    private static final Long serialVersionUID = 1L;
    private String id;
    private Integer age;
    private String name;
}

创建序列化和反序列化方法

public class Client {
    public static void main(String[] args) throws Exception {
//        ser();
        dser();
    }

    /**
     * 序列化
     * @throws Exception
     */
    static void ser() throws Exception {
        //age会序列化默认值null
        Car car = new Car()
                .setId("13")
                .setName("小红");
        FileOutputStream fileOut =new FileOutputStream("C:\\Users\\admin\\Desktop\\file_upload\\car.ser");
        ObjectOutputStream out = new ObjectOutputStream(fileOut);
        out.writeObject(car);
        out.close();
        fileOut.close();
        System.out.println("--------序列化完成----------");
    }

    /**
     * 反序列化
     * @throws Exception
     */
    static void dser() throws Exception {
        FileInputStream fileIn = new FileInputStream("C:\\Users\\admin\\Desktop\\file_upload\\car.ser");
        ObjectInputStream in = new ObjectInputStream(fileIn);
        Car car = (Car) in.readObject();
        in.close();
        fileIn.close();
        System.out.println("---------反序列化完成-------");
        System.out.println(car);
    }

如果需要特殊定制序列化, 这种情况,只有指定的字段被处理,其他的字段丢失。 注意,读写的顺序对应,否则值就串了

我们在Car类添加两个方法

@Data
@Accessors(chain = true)
public class Car implements Serializable {
    private static final Long serialVersionUID = 1L;
    private String id;
    private Integer age;
    private String name;

    private void writeObject(ObjectOutputStream out) throws IOException {
        System.out.println("writeObject");
        out.writeObject(id);
        out.writeInt(age == null? 1:age);
        out.writeObject(name);
    }

    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
        System.out.println("readObject");
        this.id= in.readObject().toString();
        this.age = in.readInt();
        this.name =in.readObject().toString();
    }
}

序列化的代价

1、一旦确认了序列化的形式。后续任何变动,都可能导致使用这个格式进行反序列化的程序发生错误。

2、默认的序列化会导出私有字段这一点与封装特性相违背

程序不同版本之间,因为优化或者修复 bug ,改变内部的私有字段非常常见

这些本来应该被隐藏在具体实现中,调用者不需要也不应该关心,但默认的序列化会导出这些数据

3、序列化是另一种创建对象实例的机制(clone同样也是绕过了构造器)

因为序列化绕过了构造器很容易发生“在构造器中做了安全前提检查,但因为序列化根本不走构造器,所以绕过了这种安全检查”


自定义序列化形式

假设,有个类 Cat 实现了序列化,并且将数据序列化,保存到硬盘或某个位置

之后,Cat 考虑应该继承一个 Animal 父类。并且,Animal 的信息自然也应该属于序列化的一部分

但注意:我们序列化的时候可没有保存 Animal 信息

这时候怎么办?

java 给出的方案就是 readObjectNoData

也就是,没有流,给一个默认的恢复方案。java 会调用 Animal 的 readObjectNoData

当然,还有一种类似的情况。就是本来就继承 Animal ,但Animal 忘了声明序列化。

在保存数据之后想起来了,加上了 Serializable ,但因为之前的数据中也没有 序列化父类信息

所以恢复的时候也应该从 readObjectNoData设置

总结: 此方法用在序列化的父类中。当子类序列化时候,父类未参与。

但后续反序列化的时候根据新规则,父类需要参与反序列化,可实际信息中,父类并没有,所以,需要用 readObjectNoData

回到刚才的话题,为什么需要书写 readObjectNoData?

因为,如果我们去掉这个方法,父类就默认不处理数据了,这时候可能处于错误的状态中

当然,如果父类不面临这种问题,就不需要处理

例子:

假设现在Car需要继承Animal

@Data
@Accessors(chain = true)
public class Animal{
    public int pid;
}
@Data
@Accessors(chain = true)
public class Car extends Animal implements Serializable {
    private static final Long serialVersionUID = 1L;
    private String id;
    private Integer age;
    private String name;

    private void writeObject(ObjectOutputStream out) throws IOException {
        System.out.println("writeObject");
        out.writeObject(id);
        out.writeInt(age == null? 1:age);
        out.writeObject(name);
    }

    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
        System.out.println("readObject");
        this.id= in.readObject().toString();
        this.age = in.readInt();
        this.name =in.readObject().toString();
    }
}

由于之前序列化的时候没有继承Animal,所以反序列化的时候,获取Animal的字段均为默认值,这个时候如果要自定义给Animal赋值就需要这样做

@Data
@Accessors(chain = true)
public class Animal implements Serializable {
    private static final Long serialVersionUID = 1L;
    public int pid;

    private void readObjectNoData() throws IOException, ClassNotFoundException {
        System.out.println("readObjectNoData");
        this.pid = 1;
    }
}

我们给父类的pid赋值1 ,然后调用反序列化方法

public class Client {
    public static void main(String[] args) throws Exception {
//        ser();
        dser();
    }

    /**
     * 序列化
     * @throws Exception
     */
    static void ser() throws Exception {
        //age会序列化默认值null
        Car car = new Car()
                .setId("13")
                .setName("小红");
        FileOutputStream fileOut =new FileOutputStream("C:\\Users\\admin\\Desktop\\file_upload\\car.ser");
        ObjectOutputStream out = new ObjectOutputStream(fileOut);
        out.writeObject(car);
        out.close();
        fileOut.close();
        System.out.println("--------序列化完成----------");
    }

    /**
     * 反序列化
     * @throws Exception
     */
    static void dser() throws Exception {
        FileInputStream fileIn = new FileInputStream("C:\\Users\\admin\\Desktop\\file_upload\\car.ser");
        ObjectInputStream in = new ObjectInputStream(fileIn);
        Car car = (Car) in.readObject();
        in.close();
        fileIn.close();
        System.out.println("---------反序列化完成-------");
        System.out.println(car);
        System.out.println(car.pid);
    }
}

可以看到,父类的pid字段被成功赋值 

readObjectNoData
readObject
---------反序列化完成-------
Car(id=13, age=1, name=小红)


补充说明

以上的情况是出于谨慎考虑,实际上, 现在很多框架都不会明确依赖序列化

更多的是Object - json 的路线或者使用 rest api

现在的 SpringBoot 或 SpringCloud 项目,一般不会被序列化问题所困扰

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值