Java序列化详解

本文大都参考文章:https://juejin.im/post/5ce3cdc8e51d45777b1a3cdf

一、 序列化的含义、意义、使用场景

  • 序列化:将堆内存中对象持久化,写入IO流中
  • 反序列化:从IO流中恢复对象
  • 意义:序列化机制允许将实现序列化的对象转换为字节序列,这些字节序列可以保存在磁盘上,还可以通过网络传输,以达到以后可以恢复为原始对象的目的。序列化机制使得对象可以脱离程序的允许而独立存在
  • 使用场景:
    • 所有在网络上传输的对象都必须是可序列化的。比如RMI(Remote Method Invoke)远程方法调用,传入的参数或返回的对象都必须要是可序列化的。
    • 所有需要保存在磁盘上的Java对象都必须要是可序列化的
  • 使用建议:程序创建的每个JavaBean类都实现Serializable接口

二、序列化的实现方式

两个相关的重要的接口SerializableExternalizable

1. Serializable

1.1 普通序列化和反序列化

  • 前提条件:首先需要一个类实现接口Serializable
  • 序列化步骤:
    1. 创建一个ObjectOutputStream输出流
    2. 调用ObjectOutputStream输出流对象的writeObject方法输出可序列化对象
public class Person implements Serializable {

    private String name;
    private int age;

    public Person(String name, int age){
        this.name = name;
        this.age = age;
    }
    @Override
    public String toString() {
        return "Person{" + "name=" + name + '\'' + ",age = " + age + "}";
    }
}
  • 序列化代码:
public class WriteObject {
    public static void main(String[] args) {
        try {
            //0. 创建一个ObjectOutputStream输出流
            ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("object.txt"));
            //1. 将对象序列化到文件s
            Person person = new Person("xiaozhang", 12);
            oos.writeObject(person);
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}
  • 运行结果:在项目的根目录下生成了序列化文件
    在这里插入图片描述

  • 反序列化步骤:

    1. 创建一个ObjectInputStream输入流对象,传入指定的序列化后的文件
    2. 调用ObjectInputStream对象的readObject()方法得到序列化后的对象
public class ReadObject {
    public static void main(String[] args) {
        try {
            //0. 创建ObjectInputStream
            ObjectInputStream ois = new ObjectInputStream(new FileInputStream("object.txt"));
            Person person = (Person) ois.readObject();
            System.out.println(person);
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}
  • 输出结果
    在这里插入图片描述

  • 问题:反序列化的过程,对象是如何创建的?是由对应的类的构造方法创建的吗?

    • 通过在类的构造方法添加一行输出,再次运行上述代码来验证一下
      在这里插入图片描述
  • 输出结果
    在这里插入图片描述

  • 发现结果并没有通过构造函数。

  • 反序列化并不会调用构造方法,反序列化的对象是由JVM 自己生成的对象,不会通过构造方法

1.2 成员是引用的序列化

如果一个实现了Serializable接口的类成员不仅包含了基本数据类型和String类型,也包含了引用类型,那么这个引用类型对应的类也必须是可序列化的,否则会导致该类无法序列化

实验:新增Teacher类,去除Person类的Serializable接口

  • person
public class Person  {

    private String name;
    private int age;

    public Person(String name, int age){
        System.out.println("反序列化过程,构造方法被调用了吗?");
        this.name = name;
        this.age = age;
    }

    @Override
    public String toString() {
        return "Person{" + "name=" + name + '\'' + ",age = " + age + "}";
    }
}
	
  • teacher
public class Teacher implements Serializable {

    String name;
    Person person;
    public Teacher(String name, Person person){
        this.name = name;
        this.person = person;
    }

}
  • WriteObject
public class WriteObject {
    public static void main(String[] args) {
        try {
            //0. 创建一个ObjectOutputStream输出流
            ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("object.txt"));
            //1. 将对象序列化到文件
            Teacher teacher = new Teacher(1, new Person("zhangsan",11));
            oos.writeObject(teacher);
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}
  • 输出
    在这里插入图片描述

  • Person不可序列化导致Teacher不可序列化

1.3 同一对象多次序列化的机制

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

    • 不会
  • 将上述代码中的Person实现序列化接口Serializable

  • 然后分别先进行序列化

public class WriteObject {
    public static void main(String[] args) {
        try {
            //0. 创建一个ObjectOutputStream输出流
            ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("teacher.txt"));
            //1. 将对象序列化到文件
            Person person = new Person("路飞", 20);
            Teacher t1 = new Teacher("雷利", person);
            Teacher t2 = new Teacher("红发香克斯", person);
            //2.依此将上述对象写入输出流
            oos.writeObject(t1);
            oos.writeObject(t2);
            oos.writeObject(person);
            oos.writeObject(t2);

        }catch (Exception e){
            e.printStackTrace();
        }
    }
}
  • 然后进行反序列化
public class ReadObject {
    public static void main(String[] args) {
        try {
            //0. 创建ObjectInputStream
            ObjectInputStream ois = new ObjectInputStream(new FileInputStream("teacher.txt"));
            //1. 反序列化的顺序与序列化的顺序保持一致
            Teacher t1 = (Teacher) ois.readObject();
            Teacher t2 = (Teacher) ois.readObject();
            Person person = (Person) ois.readObject();
            Teacher t3 = (Teacher) ois.readObject();
            System.out.println(t1 == t2);
            System.out.println(t1.getPerson() == person);
            System.out.println(t2.getPerson() == person);
            System.out.println(t2 == t3);
            System.out.println(t1.getPerson() == t2.getPerson());

        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

  • 反序列化输出
    在这里插入图片描述

  • 由此可以更深入的理解最开始的问题了,Java序列化同一对象的时候,并不是多次序列化操作就能得到多个对象,而是不管序列化多少次,最终只只会得到一个对象

1.4 Java序列化算法要点

  • 所有保存到磁盘的对象都有一个序列化编码号
  • 当尝试对一个对象进行序列化操作前,会检查该对象是否以及被序列化过,只有该对象从未被序列化过,才会将对象序列化为字节序列。
  • 如果该对象已经序列化了,则直接输出编号即可。
  • 上述序列化过程的图解

在这里插入图片描述

1.5 Java序列化算法存在的问题

  • 由于Java序列化算法不会重复序列化同一个对象,只会记录已序列化对象的编号。如果序列化一个可变对象,当该对象的内容被改变时,再次序列化,并不会将内容改变后的对象转换为字节序列
public class WriteObject {
    public static void main(String[] args) {
        try {
            //0. 创建一个ObjectOutputStream输出流
            ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("teacher.txt"));
            ObjectInputStream ios = new ObjectInputStream(new FileInputStream("teacher.txt"));
            //1. 第一次序列化Person
            Person person = new Person("张三", 11);
            oos.writeObject(person);
            System.out.println(person);

            //2. 修改对象
            person.setName("李四");
            System.out.println(person);
            //3. 第二次序列化
            oos.writeObject(person);
            //4. 依此反序列化
            Person p1 = (Person) ios.readObject();
            Person p2 = (Person) ios.readObject();
            System.out.println(p1 == p2);
            System.out.println(p1.getName());
            System.out.println(p2.getName());
            System.out.println(p1.getName() == p2.getName());

        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

  • 输出:
    在这里插入图片描述

1.6 可选的自定义序列化

1.6.1 transient

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

  • 下面的实验将说明这个问题
  • Person类
public class Person implements Serializable  {


    private transient String name;
    private transient int age;
    private int height;
    private transient boolean singlehood;

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public int getHeight() {
        return height;
    }

    public void setHeight(int height) {
        this.height = height;
    }

    public boolean isSinglehood() {
        return singlehood;
    }

    public void setSinglehood(boolean singlehood) {
        this.singlehood = singlehood;
    }

    public Person(String name, int age){
        this.name = name;
        this.age = age;
    }
    public void setName(String name){
        this.name = name;
    }
    public String getName(){
        return name;
    }

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", height=" + height +
                ", singlehood=" + singlehood +
                '}';
    }
}
  • 测试类
public class TransientTest {

    public static void main(String[] args) {

        try {
            //0. 新建输入流和输出流
            ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("person.txt"));
            ObjectInputStream ois = new ObjectInputStream(new FileInputStream("person.txt"));

            Person person = new Person("张三", 11);
            person.setHeight(111);
            person.setSinglehood(true);
            System.out.println("序列化前:"+ person);
            oos.writeObject(person);
            Person person1 = (Person)ois.readObject();
            System.out.println("反序列化结果:" + person1);
        }catch (Exception e){
            e.printStackTrace();
        }
    }

}
  • 输出
    在这里插入图片描述
1.6.2 可选的自定义序列化(暂时没懂)

使用transient虽然简单,但是将此属性完全隔离在了序列化之外,非常的不灵活。Java提供了可选的自定义序列化,可以控制序列化的方式,或者为序列化数据进行编码加密等。

  • 一般通过重写下面的方法来实现
private void writeObject(java.io.ObjectOutputStream out) throws IOException;
private void readObject(java.io.ObjectIutputStream in) throws IOException,ClassNotFoundException;
private void readObjectNoData() throws ObjectStreamException;
  • Person中定义私有的writeObject()readObject()方法
 private String name;
    private int age;
    private int height;
    private boolean singlehood;



    public void writeObject(ObjectOutputStream out) throws IOException{

        //0. 将name属性反转写入二进制流
        out.writeObject(new StringBuffer(this.name).reverse());
        out.writeInt(age);
        System.out.println("自定义的writeObject是否被调用");
    }

    public void readObject(ObjectInputStream ins) throws IOException, ClassNotFoundException {

        //1. 将读出的字符串反转恢复出来
        this.name = ((StringBuffer)ins.readObject()).reverse().toString();
        this.age = ins.readInt();
        System.out.println("自定义的readObject是否被调用");
    }

  • 然后再对这个类的对象进行序列化和反序列化,但是不知道为什么,并没有调用自定义的相关方法
1.6.3 更彻底的自定义序列化
  • 两个重要方法
ANY-ACCESS-MODIFIER Object writeReplace() throws ObjectStreamException;  
ANY-ACCESS-MODIFIER Object readResolve() throws ObjectStreamException;
  • writeReplace():在序列化时,会优先调用此方法, 在调用writeObject()方法。此方法可以将任意对象代替位目标对象
public class Person implements Serializable  {


    private String name;
    private int age;

    private Object writeReplace() throws ObjectStreamException{
        ArrayList<Object> list = new ArrayList<>();
        list.add(this.name);
        list.add(this.age);
        return list;
    }
  • 测试
public class Test {

    public static void main(String[] args) {
        try {
            ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("test.txt"));
            ObjectInputStream ois = new ObjectInputStream(new FileInputStream("test.txt"));
            Person person = new Person("xiaozhang", 11);
            System.out.println("序列化之前:" + person);
            oos.writeObject(person);
            ArrayList list = (ArrayList)ois.readObject();
            System.out.println("反序列化之后:" + list);
        }catch (Exception e){
            e.printStackTrace();
        }
    }

}
  • 输出
    在这里插入图片描述

  • 可见序列化的时候,通过writeReplace()方法,修改了序列化后的对象

  • readResolve()反序列化时替换反序列化的对象,反序列化出来的对象被丢弃,在readObject()后被调用

public class Person implements Serializable  {


    private String name;
    private int age;

    public Object readResolve() throws Exception{
        return new Person("xiaoming",11);
    }

  • 测试
public class Test {

    public static void main(String[] args) {
        try {
            ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("test.txt"));
            ObjectInputStream ois = new ObjectInputStream(new FileInputStream("test.txt"));
            Person person = new Person("xiaozhang", 11);
            System.out.println("序列化之前:" + person);
            oos.writeObject(person);
            Person person1 = (Person) ois.readObject();
            System.out.println("反序列化之后:" + person1);
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}
  • 输出
    在这里插入图片描述

2. Externalizable:强制自定义序列化

  • 通过实现Externalizable接口,必须实现writeExternalreadExternal方法
public class ExPerson implements Externalizable {

    private String name;
    private int age;

    //0. 注意按此种方式实现序列化,必须添加无参的Public的构造器
    public ExPerson(){};

    public ExPerson(String  name, int age){
        this.name = name;
        this.age = age;
    }
    @Override
    public void writeExternal(ObjectOutput out) throws IOException {
        //1. 将name反转后写入二进制流
        StringBuffer reverse = new StringBuffer(name).reverse();
        System.out.println("写入时反转name:" + reverse.toString());
        out.writeObject(reverse);
        out.writeInt(age);
    }

    @Override
    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
        //2.读出字符串的时候也需要反转
        this.name = ((StringBuffer) in.readObject()).reverse().toString();
        System.out.println("读出时再次反转:" + name);
        this.age = in.readInt();
    }

    @Override
    public String toString() {
        return "ExPerson{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}
public class Test {

    public static void main(String[] args) {
        try {
            ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("test.txt"));
            ObjectInputStream ois = new ObjectInputStream(new FileInputStream("test.txt"));
            ExPerson exPerson = new ExPerson("xiaozhang", 111);
            System.out.println("序列化之前:" + exPerson);
            oos.writeObject(exPerson);
            ExPerson ep = (ExPerson) ois.readObject();
            System.out.println("序列化之后:"+ ep);
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}
  • 输出:
    在这里插入图片描述

2.1要点

  • 必须实现接口Externalizable的两个方法。writeExternalreadExternal
  • 必须提供public的空参构造方法,这是因为在反序列化的时候需要反射的创建对象

3. 两种序列化对比

在这里插入图片描述

三、序列化版本号:serialVersionUID

反序列化必须有class文件,但是随着项目升级,class文件也会升级,序列化怎么保证不同版本之间的兼容性呢?

  • Java提供了一个private static final long serialVersionUID的序列化版本号。只要版本号相同,即使更改了序列属性,对象也可以被正确的序列化回来。
public class Person implements Serializable  {

    private static final long serialVersionUID = 1L;

    private String name;
    private int age;

  • 如果反序列化使用的class版本号与序列化时使用的不一致,反序列化会报InvalidClassException异常

  • 序列化版本号可自由指定,如果不指定,JVM会根据类信息自己计算一个版本号,这样随着class的升级,就无法正确反序列化;

  • 不指定版本号另一个明显隐患是,不利于jvm间的移植,可能class文件没有更改,但不同jvm可能计算的规则不一样,这样也会导致无法反序列化

需要修改serialVersionUID的三种情况

  • 如果只是修好方法,反序列化不容易影响,则无需修改
  • 如果只是修改了静态变量、瞬时变量(transient修饰的变量),反序列化不受影响
    • 静态变量也不会被序列化,但是反序列化的时候可以正常的赋值,因为静态变量属于类,直接从类中获取即可
  • 如果修改了非瞬态变量,则可能导致反序列化失败。如果新类中实例变量的类型与序列化时类的类型不一致,则会反序列化失败,这时候需要更改serialVersionUID如果只是新增了实例变量,则反序列化回来新增的是默认值;如果减少了实例变量,反序列化时会忽略掉减少的实例变量。

四、总结

  1. 有需要网络传输的对象都需要实现序列化接口,通过建议所有的javaBean都实现Serializable接口。
  2. 对象的类名、实例变量(包括基本类型,数组,对其他对象的引用)都会被序列化;方法、类变量、transient实例变量都不会被序列化。
  3. 如果想让某个变量不被序列化,使用transient修饰。
  4. 序列化对象的引用类型成员变量,也必须是可序列化的,否则,会报错。
    反序列化时必须有序列化对象的class文件。
  5. 当通过文件、网络来读取序列化后的对象时,必须按照实际写入的顺序读取。
  6. 单例类序列化,需要重写readResolve()方法;否则会破坏单例原则。
  7. 同一对象序列化多次,只有第一次序列化为二进制流,以后都只是保存序列化编号,不会重复序列化。
  8. 建议所有可序列化的类加上serialVersionUID 版本号,方便项目升级。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值