Java序列化

338 阅读8分钟

1.简介

今天来学习一下Java序列化,相对来说:比较简单,初步了解为主;

2.背景介绍

2.1 什么是Java序列化

序列化,就是把对象改成二进制的过程,可以保存到磁盘或者网络发送; 反序列化:就是把二进制转换为对象的过程;

2.2 序列化应用场景

  1. 变成二进制,就可以把数据进行网络传输(json也是序列化协议的一种)
  2. 如果,内存中的数据不够用了,可以通过序列化先放到磁盘中进行保存
  3. 加密,如果传输的数据需要进行加密,序列化后,进行加密;接受数据后,解密;然后反序列化拿到数据;

3.代码演示

先来一个实现了Serializable接口的Student对象

3.1 基础功能实现

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Student implements Serializable {
    private int id;
    private String name;
    
}

实现序列化

public class ObjectOutputStreamTest01 {
    public static void main(String[] args) throws Exception {
        // 1.创建java对象
        Student s = new Student(1111, "zhangsan");
        // 2.序列化
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("students"));

        // 3.序列化对象
        oos.writeObject(s);

        // 4.刷新
        oos.flush();

        // 5.关闭
        oos.close();
    }
}

这个时候,在我的项目中,就出现了一个序列化完毕后的students文件

image.png
然后,我们想要读取这个文件,代码如下:

public class ObjectInputSteeamTest01 {
    public static void main(String[] args) throws Exception{
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("students"));
        // 开始反序列化,读
        Object obj = ois.readObject();
        // 反序列化回来是一个学生对象,所以会调用学生对象的toString方法
        System.out.println(obj);
        ois.close();
    }
}

结果如下:

image.png
反序列化成功;

3.2 多个对象一起序列化

现在,有一个这样的需求,就是要把多个对象一起进行序列化;如果,调用两次oos.writeObject(s);是不行的;正确解决方法把对象放到list中,然后,序列化List;
User对象代码: 注意,User也需要实现Serializable接口

@Data
@AllArgsConstructor
@NoArgsConstructor
public class User implements Serializable {
    private int id;
    private String name;
}

序列化代码:

public class ObjectOutputStreamTest02 {
    public static void main(String[] args) throws Exception{
        ArrayList<User> userList = new ArrayList<>();
        userList.add(new User(111,"zhangsan"));
        userList.add(new User(222,"lisi"));
        userList.add(new User(333,"wanwu"));

        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("users"));

        // 序列化集合,集合对象中放了很多对象
        oos.writeObject(userList);
    }
}

反序列化代码:

public class ObjectInputSteeamTest02 {
    public static void main(String[] args) throws Exception{
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("users"));
        // 开始反序列化,读
        List<User> obj = (List<User>)ois.readObject();
        for (User user: obj
             ) {
            System.out.println(user);
        }
        System.out.println(obj);
        ois.close();
    }
}

经过测试成功;

3.3 希望某些变量不序列化

如果,我们希望某些变量不序列化的话,就可以使用transient 来修饰该变量;
把Student类中name变量用transient来进行修饰,然后,重新进行序列化,再进行反序列化;(代码和3.1一样)结果如下:

Student(id=1111, name=null)

证明,在序列化的时候,没有把name的属性传入;

4.序列化版本号的作用

首先,Serializable 是一个标志接口,所谓的标志接口,就是让虚拟机知道;Java虚拟机看到这个接口后,如果,我们没有提供序列化版本号的话,会为该类自动生成一个序列化版本号。
如果,我们没有定义序列化版本号;该类发生了代码的修改;此时,之前的序列化文件就不能被反序列化了;因为,源代码修改后,需要重新编译,生成全新的字节码文件;并且,class文件再次运行的时候,Java虚拟机机会重新生成一个序列化版本号;这就导致之前的序列化版本号和现在版本号是不同的,所以,无法被反序列化;
注意:Java语言中,针对实现了Serializable接口的类,采用如下机制来区分类:

  1. 首先,通过类名进行对比,如果,类名不一样,肯定不是同一个类;
  2. 如果,类名一样,在判断序列化版本号是否一样 4.1 这种自动生成版本号有什么缺陷?
    一旦代码确定之后,不能进行后续的修改,因为只要修改,必然会重新编译,此时会生成全新的序列化版本号,这个时候java虚拟机会认为这是一个全新的类(这样就不好了)
    小结论:凡是一个类实现了Serializable接口,建议给该类提供一个固定不变的序列化版本号;这样,以后这个类即使代码修改了,但是版本号不变,java虚拟机会认为是同一个类;
    例如:我们的HashMap ArrayList 肯定随着版本的变化是会发生修改的,如果,修改后,之前的序列化文件不能被反序列化回来,就麻烦了 所以,一般JDK提供的类 如String、HashMap、ArrayList 都是可以序列化的。并且,序列化的ID都是固定的,这样,这算发生了版本升级,也不会导致之前数据无法被反序列化的问题;
    测试如下:给Student类添加序列化ID,生成序列化文件;然后,对其增减字段看看能否进行反序列化;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Student implements Serializable {
    private static final long serialVersionUID = -6849794470754667733L;
    private int id;
    private transient String name;
}

序列化完毕后,对其字段进行修改,结果如下:

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Student implements Serializable {
    private static final long serialVersionUID = -6849794470754667733L;
    private int id;
    // private transient String name;
    private transient String phone;
}

然后,进行反序列化,结果如下: 符合预期;

Student(id=1111, phone=null)

5.Externalizable

如果,我们不想序列化的字段太多了,如果,每个字段都加transient关键字的话,会比较麻烦,为了解决这个问题,我们可以通过实现Externalizable接口来进行解决;
Externalizable继承Serializable ;对其使用时,只要声明一个空的构造方法,覆写writeExternal和readExternal方法即可;

public interface Externalizable extends java.io.Serializable {

    void writeExternal(ObjectOutput out) throws IOException;

    void readExternal(ObjectInput in) throws IOException, ClassNotFoundException;
}

5.1 测试

Externalizable实现类:

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Person implements Externalizable {
    private static final long serialVersionUID = -23537246523447182L;
    private int age;
    private String name;


    @Override
    public void writeExternal(ObjectOutput out) throws IOException {
        out.writeInt(age);
        out.writeObject(name);
    }

    @Override
    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
        age = in.readInt();
        Object o = in.readObject();
        if (o instanceof String) {
            name = (String)o;
        }
    }
}

测试类:

public class ExternalizableTest {
    public static void main(String[] args) throws Exception{
        // 1. 创建对象
        Person p = new Person(11, "小卢");
        // 2.序列化
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("person"));

        // 3.序列化对象
        oos.writeObject(p);

        // 4.刷新
        oos.flush();

        // 5.关闭
        oos.close();
        // 开始反序列化,读
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("person"));

        Object obj = ois.readObject();
        System.out.println(obj);
    }
}

结果如下:

Person(age=11, name=小卢)

使用Externalizable注意点

  1. 使用Externalizable序列化,必须要声明一个无参构造,否则会报no valid constructor的异常
  2. 使用Externalizable序列化,相当于所有的成员变量都添加了transient关键字,如果想要序列化或者反序列化哪些字段需要自己重写writeExternal和readExternal方法。
  3. 使用Externalizable序列化,transient关键字可以用,但没有任何效果。

6.其他的一些注意点

6.1 对象序列化后再次序列化
对一个对象序列化之后,为了节省空间,只会写入一次。再写入时,后面追加的是该对象的引用;
还是,以刚刚的例子:

    public static void main(String[] args) throws Exception{
        // 1. 创建对象
        Person p = new Person(11, "小卢");
        // 2.序列化
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("person"));

        // 3.序列化对象
        oos.writeObject(p);

        p.setAge(22);
        p.setName("小陈");
        
        // 序列化失败
        oos.writeObject(p);
        // 4.刷新
        oos.flush();

        // 5.关闭
        oos.close();
        // 开始反序列化,读
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("person"));

        Object p1 = ois.readObject();
        Object p2 = ois.readObject();
        System.out.println(p1);
        System.out.println(p2);

    }

经过测试结果为:

Person(age=11, name=小卢)
Person(age=11, name=小卢)

解决方案:

  1. 修改对象变量后,不使用oos.writeObject(person);使用oos.writeUnshared(person);(不管,这个对象之前是否被序列化过,都会当作新出现的对象执行序列化)
  2. 修改对象变量后,仍然使用oos.writeObject(person);但在这之前要调用oos.reset();重置流。 输出结果:
Person(age=11, name=小卢)
Person(age=22, name=小陈)

6.2 子类实现Serilizable 接口 父类却为实现
如果,父类实现Serilizable接口,那么,子类肯定也是实现了它。所以也可以序列化;如果是子类实现了Serializable接口,而父类却没有,就会报错;所以,需要让父类实现该接口,或者,在父类中声明一个无参构造;
6.3 修改成员变量
我们之前测试,增加成员变量的值,或者删除成员变量的值,只要有序列化号,则可以进行序列化或者反序列化;那么,对原来成员变量的类型进行修改,那反序列化就会报错;(其实理解一下,就可以明白)
所以,我们在序列化类成员变量类型进行修改以后,也需要对serialVersionUID进行修改;
6.4 序列化会破坏单例模式

/**
 * @auther:lgb
 * @Date: 2021/5/12
 */
public class PersonUtils implements Serializable {
    private static PersonUtils mInstance;

    private PersonUtils() {
        
    }

    public static PersonUtils getInstance() {
        if (mInstance == null) {
            synchronized (PersonUtils.class) {
                if (mInstance == null) {
                    mInstance = new PersonUtils();
                }
            }
        }
        return mInstance;
    }

    public static void main(String[] args) throws Exception{
        PersonUtils instance = PersonUtils.getInstance();
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("single"));
        // 3.序列化对象
        oos.writeObject(instance);
        // 4.刷新
        oos.flush();
        // 5.关闭
        oos.close();
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("single"));
        // 开始反序列化,读
        Object obj = ois.readObject();
        // 反序列化回来是一个学生对象,所以会调用学生对象的toString方法
        System.out.println("单例获取到对象:" + instance.hashCode());
        System.out.println("实例化获取到的对象:" + obj.hashCode());
        ois.close();
    }
}

注意:反射也会破坏单例,因为反射都可以拿到和修改一个类的所有信息;