序列化

序列化

什么是序列化和反序列化?

  • 序列化:[将类的实例对象转换成二进制字节序列的过程]{.red}
  • 反序列化:根据二进制字节序列信息重新构建类的实例对象的过程

为什么要使用序列化?

  • 核心:[不同的 Java 虚拟机之间共享实例对象的解决方案]{.red}

  • [持久化]{.pink}:对象的实例变量可以通过序列化成二进制字节序列长期保存在磁盘中,脱离进程独立存在

  • [网络传输]{.pink}:不同进程之间进行通信需要传输数据,只有二进制数据能够在网络中传输,对象想要在网络中传输必须被序列化

    • Socket:客户端和服务器之间进行通信
    • RMI :采用分布式架构的各个计算机之间进行通信
  • 细节:

    • 通常建议所有类都实现序列化接口从而方便对象能够进行网络传输或者持久化存储
    • 序列化核心目的是为了共享实例对象而不是为了持久化存储,毕竟持久化存储已经交给了数据库完成,所以如果你的程序不涉及网络那么序列化也就没什么用了

如何实现序列化?

  • Serializable([标志性接口]{.red})

    • 持久化

      /*实现接口*/
      class Person implements Serializable{
      /*序列化编号:后文将会提到它的作用*/
      private static final long serialVersionUID = 114514L;
      private String name;
      private String age;
      private String gender;

      public Person(String name, String age, String gender){
      /*成员变量初始化...*/
      }
      }

      public static void main(String[] args) throws IOException, ClassNotFoundException{
      /*序列化目的:文件、网络、分布传输*/
      File file = new File("files/person");
      /*创建序列化输出流*/
      ObjectOutputStream os = new ObjectOutputStream(
      new FileOutputStream(
      file));
      /*等待被序列化的对象*/
      Person person = new Person("张三", "24","男");
      /*如果文件不存在就创建文件*/
      if (!file.exists()){
      file.createNewFile();
      }
      /*调用序列化输出流将对象写入文件进行持久化保存*/
      os.writeObject(person);
      /*关闭序列化输出流*/
      os.close();
      /*创建序列化输入流*/
      ObjectInputStream is = new ObjectInputStream(
      new FileInputStream(file));
      /*需要进行强制类型转换才可以得到原对象*/
      Person newPerson = (Person) is.readObject();
      System.out.println(newPerson);
      is.close();
      }
    • 网络传输

      吐槽一句,明明序列化技术主要就是用于网络传输,但是为什么那么多讲序列化的博客都没有将网络传输作为例子讲呢?

      /*服务器和客户端进行交互的例子*/
      /*需要发送的消息*/
      public class Message implements Serializable{
      private static final long serialVersionUID = 114514L;
      private String message;
      private String date;
      /*类的其余方法不在此列出,不重要*/
      }
      /*服务器*/
      public class Server{
      /*服务器端口号*/
      private static final int DEFAULT_SERVER_PORT = 4396;
      /*服务器端*/
      public static void main(String[] args) throws IOException, ClassNotFoundException{
      // 创建服务器处理请求的端口 Socket
      ServerSocket server = null;
      // 服务器读取客户端传递的数据
      ObjectInputStream reader = null;
      // 服务器返回给客户端的数据
      ObjectOutputStream writer = null;

      server = new ServerSocket(DEFAULT_SERVER_PORT);
      System.out.printf("服务器[%d]:%s\n", DEFAULT_SERVER_PORT, "启动完成...");
      /*服务器始终运行*/
      while (true){
      Socket client = server.accept();
      System.out.printf("客户端[%d]:%s\n", client.getPort(), "建立连接");
      /*传递的消息对象*/
      Message message = null;
      reader = new ObjectInputStream(client.getInputStream());
      writer = new ObjectOutputStream(client.getOutputStream());
      /*读出对象*/
      while ((message = (Message) reader.readObject()) != null)
      {
      // 格式化输出客户端发送的消息
      System.out.printf("客户端[%d]:%s\n", client.getPort(), message.getMessage() + "\t" + message.getDate());
      // 向客户端写入对象
      writer.writeObject(new Message("服务器[" + DEFAULT_SERVER_PORT + "]:处理完毕"));
      // 确保缓冲区中所有的内容都被推出: writer 关闭后会自动推出缓冲区中剩余的内容
      writer.flush();
      }
      System.out.printf("客户端[%d]:%s", client.getPort(), "退出连接");
      }
      reader.close();
      writer.close();
      }
      }
      /*客户端*/
      public class Client{
      private static final String DEFAULT_SERVER_HOSTNAME = "127.0.0.1";
      private static final int DEFAULT_SERVER_PORT = 4396;
      private static final String EXIT = "退出";
      public static void main(String[] args) throws IOException
      {
      // 客户端访问服务器端口: 客户端的端口是操作系统随机分配的,不需要程序员决定,只需要指定访问哪个服务器端口
      Socket server = null;
      // 客户端向服务器发送数据
      ObjectOutputStream writer = null;
      // 客户端接收服务器返回的数据
      ObjectInputStream reader = null;
      // 客户端输入需要发送的数据
      BufferedReader consoleReader = new BufferedReader(
      new InputStreamReader(
      System.in));

      server = new Socket(DEFAULT_SERVER_HOSTNAME, DEFAULT_SERVER_PORT);
      System.out.println("客户端启动成功...");
      Message message;
      String string;
      writer = new ObjectOutputStream(
      new BufferedOutputStream(
      server.getOutputStream()));
      while (!EXIT.equals(string = consoleReader.readLine())){
      /*将控制台输入的字符串封装成对象发送给服务器*/
      writer.writeObject(new Message(string));
      writer.flush();
      /*输入流只能够创建在循环内:原因不明*/
      reader = new ObjectInputStream(
      new BufferedInputStream(
      server.getInputStream()));
      /*读出服务器返回的消息对象*/
      message = (Message)reader.readObject();
      System.out.println(message.getMessage() + "\t" + message.getDate());
      }

      }
      }
  • Externalizable([需要实现两个方法]{.red})

    (1) [Externalizable 序列化必须要提供 空参构造器]{.red}:否则抛出 java.io.InvalidClassException 异常

    (2) Externalizable 序列化强制要求提供空参构造器是因为虚拟机在创建对象的时候采用 [反射机制]{.red} 会调用空参构造器创建对象,然后调用相应的 readObject 方法初始化成员变量

    /*这里仅列举文件传输的例子不再使用网络传输列子*/
    class Person implements Externalizable{
    private static final long serialVersionUID = 114514L;
    private String name;
    private int age;
    private String gender;
    /*空参构造器必须提供*/
    public Person(){}
    public Person(String name, int age, String gender){
    ...
    }
    /*必须实现以下两个方法*/
    @Override
    public void writeExternal(ObjectOutput out) throws IOException{
    /*可以自行选择需要序列化的成员变量*/
    out.writeObject(name);
    out.writeObject(age);
    out.writeObject(gender);
    }

    @Override
    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException{
    /*需要按照序列化的顺序进行反序列化*/
    this.name = (String) in.readObject();
    this.age = (int)in.readObject();
    this.gender = (String)in.readObject();
    }
    }

    public static void main(String[] args) throws IOException, ClassNotFoundException{
    File file = new File("files/person.ser");
    ObjectOutputStream os = new ObjectOutputStream(
    new FileOutputStream(
    file));
    Person person = new Person("张三", 24,"男");
    if (!file.exists()){
    file.createNewFile();
    }
    /*Externalizable 序列化有两种方式使用:这两种方式都可以将对象序列化*/
    /*第一种方式:区别于 Serializable 接口:Externalizable 是对象调用自己的方法来进行序列化*/
    person.writeExternal(os);
    /*第二种方式:采用和 Serializable 接口 一样的方式 */
    os.writeObject(person);
    os.close();

    ObjectInputStream is = new ObjectInputStream(
    new FileInputStream(file));
    /*Externalizable 反序列化对应也有两种方式使用:这两种方式都可以将对象反序列化*/
    /*第一种方式:采用和 Serializable 接口一样的方式 */
    os.readObject(person);
    /*第二种方式:需要先创建对象后再调用反序列化方法*/
    Person newPerson = new Person();
    /*区别于 Serializable 接口:Externalizable 是对象调用自己的方法来进行反序列化*/
    newPerson.readExternal(is);
    System.out.println(newPerson);
    is.close();
    }
  • 细节:

    • [序列化技术仅会将 对象的成员变量 转换成字节序列,也就意味着 静态变量和方法 都是不会被序列化的]{.red}

      从定义中也可以看出,毕竟序列化的是实例对象,显然静态属性不属于对象

    • [如果同时实现 Externalizable 和 Serializable 接口会导致 Serializable 接口直接失效]{.red}

  • 区别:[通常仍然采用 Serializable 接口进行序列化]{.red}

    • Externalizable 性能相对较好;Serializable 性能相对较差
    • Externalizable 可以 [选择性序列化成员变量]{.red} 并且可以 [定制序列化方式]{.red};Serializable [默认序列化所有成员变量]{.red}
    • Externalizable 必须实现两个方法([实现繁琐]{.red});Serializable 是标志性接口不需要实现任何方法([实现简单]{.red})

序列化如何实现的?

1af32568725763ed6be092c20fe49bfc.png
  • ObjectStreamClass 类:保存对象对应的 Class 对象 以及对象所有 成员变量的数据信息

    官方文档描述:用于描述类的描述符,主要包含类的名称和类的版本号(serialVersionUID),可以调用自身的 lookup 方法并且利用自身的信息构建对象

  • 序列化原理

    private void writeObject0(Object obj, boolean unshared) throws IOException {
    ...
    try {
    // 构建相关对象之前需要完成的相关检查事项
    int h;
    if ((obj = subs.lookup(obj)) == null) { // 检查当前对象是否为:调用 writeNull 方法后返回
    ...
    } else if (!unshared && (h = handles.lookup(obj)) != -1) { // 检查当前对象是否已经被序列化过:调用 writeHandle 方法后返回
    ...
    } else if (obj instanceof Class) { // 检查当前对象是否为 Class 对象:调用 writeClass 方法后返回
    ...
    } else if (obj instanceof ObjectStreamClass) { // 检查当前对象是否为 ObjectStreamClass 对象 writeClassDesc 方法后返回
    ...
    }

    Object orig = obj;
    Class<?> cl = obj.getClass(); // 获取当前对象的 Class 对象信息
    ObjectStreamClass desc;
    for (;;) {
    Class<?> repCl;
    desc = ObjectStreamClass.lookup(cl, true); // 利用当前对象的 Class 对象构建 ObjectStreamClass 对象
    if (!desc.hasWriteReplaceMethod() || // 利用 ObjectStreamClass 对象判断该对象是否可以序列化
    (obj = desc.invokeWriteReplace(obj)) == null || // 接着判断 ObjectStreamClass 对象是否和具体的对象关联
    (repCl = obj.getClass()) == cl) // 最后判断 ObjectStreamClass 对象关联的对象是否和我们传入的对象为同一个
    {
    break;
    }
    cl = repCl;
    }
    ...

    if (obj instanceof String) { // 字符串类型有相应的序列化方法可以直接被序列化
    writeString((String) obj, unshared);
    } else if (cl.isArray()) { // 数组类型有相应的序列化方法可以直接被序列化
    writeArray(obj, desc, unshared);
    } else if (obj instanceof Enum) { // 枚举类型有相应的序列化方法可以直接被序列化
    writeEnum((Enum<?>) obj, desc, unshared);
    } else if (obj instanceof Serializable) { // 实现了 Serializable 接口的自定义类
    writeOrdinaryObject(obj, desc, unshared);
    } else {
    ...
    }
    }
    }

    private void writeOrdinaryObject(Object obj, ObjectStreamClass desc, boolean unshared) throws IOException{
    ...
    try {
    desc.checkSerialize();
    bout.writeByte(TC_OBJECT); // 先向文件中写入提示信息:表示这是一个新的对象
    writeClassDesc(desc, false); // 写入所有成员变量的类型信息
    handles.assign(unshared ? null : obj);
    if (desc.isExternalizable() && !desc.isProxy()) {
    writeExternalData((Externalizable) obj);
    } else { // 写入所有成员变量对应的实际数据信息
    writeSerialData(obj, desc);
    }
    }...
    }

    private void writeClassDesc(ObjectStreamClass desc, boolean unshared) throws IOException{
    int handle;
    if (desc == null) { // 检查 ObjectStreamClass 对象是否为空
    writeNull();
    } else if (!unshared && (handle = handles.lookup(desc)) != -1) { // 检查 ObjectStreamClass 对象是否已经存在
    writeHandle(handle);
    } else if (desc.isProxy()) { // ObjectStreamClass 对象是否为动态代理类
    writeProxyDesc(desc, unshared);
    } else { // 通常会走这个方法
    writeNonProxyDesc(desc, unshared);
    }
    }

    private void writeNonProxyDesc(ObjectStreamClass desc, boolean unshared) throws IOException
    {
    bout.writeByte(TC_CLASSDESC);
    handles.assign(unshared ? null : desc);

    if (protocol == PROTOCOL_VERSION_1) {
    desc.writeNonProxy(this);
    } else {
    writeClassDescriptor(desc); // 进入这个方法
    }
    ...
    writeClassDesc(desc.getSuperDesc(), false); // 子类序列化结束之后就会序列化父类:前提是父类可以序列化
    }

    void writeNonProxy(ObjectOutputStream out) throws IOException {
    out.writeUTF(name); // 写入类的名称
    out.writeLong(getSerialVersionUID()); // 写入序列化版本号:虽然序列化版本号是静态的,但是依然可以写入

    byte flags = 0;
    if (externalizable) {
    ...
    } else if (serializable) {
    flags |= ObjectStreamConstants.SC_SERIALIZABLE;
    }
    ...
    out.writeByte(flags); // 写入相关信息

    out.writeShort(fields.length); // 写入成员变量的数量
    for (int i = 0; i < fields.length; i++) { // 循环写入成员变量的类型信息
    ObjectStreamField f = fields[i];
    out.writeByte(f.getTypeCode());
    out.writeUTF(f.getName());
    if (!f.isPrimitive()) {
    out.writeTypeString(f.getTypeString());
    }
    }
    }
  • 反序列化原理

    	

序列化细节

  • transient(瞬时的)

    • 定义:[transient 关键字修饰的成员变量不会被序列化]{.red}:反序列化创建对象时该属性就会为默认值
    • 目的:① 某些数据传输过程是不安全的所以避免序列化 ② 某些对象就是没有序列化的必要(Socket、Thread之类的)
  • serialVersionUID(版本号):

    • 定义:每个类、Class 对象、ObjectStreamClass 对象、序列化文件都具有的版本号

    • 目的:[保证虚拟机的反序列化过程能够成功]{.red}

    • 生成方式

      • 虚拟机默认生成:[虚拟机根据对象的实际信息计算出相应的版本号]{.red},也就意味着只要对象发生“变化”版本号就很有可能一起变化,最终导致反序列化失败

        解释:也就意味着可能你修改了类中的信息,但是别人没有修改,导致你传输过去的版本号和别人的版本号不一致,最终是不可能反序列化成功的

      • 显示指定:[版本号必须为私有静态常量]{.red}(推荐使用这种方法)

    • 检验过程:

      • 从序列化文件中获取类的名字
      • 调用 Class.forname(name) 后获取到类对应的 Class 对象
      • 利用 Class 对象生成 ObjectStreamClass 对象
      • 利用 ObjectStreamClass 对象创建类的对象
    /*源码:检查是否一致*/
    void initNonProxy(ObjectStreamClass model, Class<?> cl, ClassNotFoundException resolveEx, ObjectStreamClass superDesc) throws InvalidClassException{
    long suid = Long.valueOf(model.getSerialVersionUID());
    ObjectStreamClass osc = null;
    if (cl != null) {
    // 利用 Class 对象获取到相应的 ObjectStreamClass 对象
    osc = lookup(cl, true);
    if (model.serializable == osc.serializable &&
    !cl.isArray() &&
    // 检查从序列化文件中读出的版本号是否和当前查询到的版本号一致:如果不一致将会抛出异常
    suid != osc.getSerialVersionUID()) {
    throw new InvalidClassException(...);
    }
    }
    }
  • 多次序列化同一个对象:

    • 核心:

      • [序列化过程中会检查该对象是否已经被序列化过]{.red}
      • 如果对象已经被序列化过,那么只会向流中写入一些提示信息,序列化文件不会做任何改变
    • 细节:对象被序列化和对象内容被修改是两件事情,如果此前已经序列化过该对象,后来修改对象内容后再次序列化是不会成功的,序列化文件保存的仍然是之前的信息

      /*例子*/
      public static void main(String[] args) throws IOException, ClassNotFoundException{
      File file = new File("files/person.ser");
      ObjectOutputStream os = new ObjectOutputStream(
      new FileOutputStream(
      file));
      Person person = new Person("张三", 24,"男");
      if (!file.exists()){
      file.createNewFile();
      }
      /*第一次序列化对象*/
      os.writeObject(getPerson(person));
      /*修改对象后第二次序列化对象*/
      person.setName("李四");
      os.writeObject(getPerson(person));
      os.close();

      ObjectInputStream is = new ObjectInputStream(
      new FileInputStream(file));
      /*最后反序列化对象时会发现内容没有任何改变*/
      Person newPerson = getPerson((Person) is.readObject());
      System.out.println(newPerson);
      is.close();
      }
      /*源码*/
      private void writeObject0(Object obj, boolean unshared) throws IOException{
      try {
      // handle previously written and non-replaceable objects
      int h;
      if ((obj = subs.lookup(obj)) == null) {
      ...
      /*注意这个分支就是判断当前对象是否序列化过*/
      } else if (!unshared && (h = handles.lookup(obj)) != -1) {
      /*lookup 方法就是查找当前对象被序列化:如果已经被序列化就会进入分支,如果没有就会继续执行后续方法*/
      writeHandle(h);
      return;
      } else if (obj instanceof Class) {
      ...
      } else if (obj instanceof ObjectStreamClass) {
      ...
      }
      }
      }
      /*向流中写入提示信息*/
      private void writeHandle(int handle) throws IOException {
      // 第一个信息是该对象已经存在的信息,第二个信息不知道是什么
      bout.writeByte(TC_REFERENCE);
      bout.writeInt(baseWireHandle + handle);
      }
  • 组合和继承中的序列化

    • 组合中的序列化:可序列化的类中拥有 [引用类型]{.red} 的成员变量,[该引用类型必须实现序列化接口,否则会导致整个对象都无法被序列化]{.red}

      /*例子*/
      public class Component{
      /*该类没有实现序列化的任何接口*/
      }
      public class Decorator implements Serializable{
      /*作为成员变量在其他可序列化的类中出现*/
      /*该类最终无法被实例化*/
      private Component component;
      }
      /*源码*/
    • 继承中的序列化

      • [如果父类已经实现序列化接口,那么子类同样可以被序列化]{.red}
      • 如果子类实现序列化接口,那么父类是不可以被序列化的
  • 反序列化构造对象

    • 不存在继承关系时:利用 Class 对象 + ObjectStreamClass 对象([反射机制]{.red})构造的对象而 不会调用构造方法创建的对象
    • 存在继承关系时:
      • 子类和父类都可以被序列化时:根据之前序列化的原理,显然子类和父类的信息都会被存储在序列化文件中,所有反序列化构造对象时两者都不会走构造方法
      • 子类可以序列化但是父类不能序列化时:反射构建子类的过程中同样需要初始化父类,所以显然会调用构造方法
  • 自定义序列化:

    • 方式:重写 writeObject & readObject 方法

    • 目的:[可能需要对该成员变量进行加密或者其他处理后再序列化]{.red}

      class Person implements Serializable
      {
      private static final long serialVersionUID = 114514L;
      private String name;
      private int age;
      private String gender;
      /*ArrayList 源码中就采用这种设计方式*/
      /*我们不想要序列化整个数组,因为数组中的空间并不会被全部使用,我们只希望序列化数组中的有效元素*/
      private int[] elements;
      public Person(String name, int age, String gender, int[] elements){
      ...
      }
      /*自定义序列化必须重写两个方法:writeObject、readObject*/
      /*自定义序列化如果采用了某种规则那么反序列化时也需要采用相反的那种规则*/
      private void writeObject(ObjectOutputStream out) throws IOException {
      /*反转字符串*/
      out.writeObject(new StringBuffer(this.name).reverse().toString());
      out.writeObject(age);
      out.writeObject(gender);
      /*我们只想序列化不为 0 的元素*/
      for (int i=0; i<elements.length; i++){
      if(elements[i]!=0)
      out.writeObject(elements[i]);
      }
      }

      private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException
      {
      /*需要再次反转回来*/
      this.name = new StringBuffer((String) in.readObject()).reverse().toString();
      this.age = (int) in.readObject();
      this.gender = (String) in.readObject();
      /*只需要填充就行了*/
      this.elements = new int[(int)in.readOnject()] // 通常数组的长度也需要被序列化进去否则反序列化会不知道数组有多少元素,导致抛出异常
      for (int i=0; i<size; i++){
      this.elements[i] = (int[]) in.readObject();
      }
      }

      }
Author: Fuyusakaiori
Link: http://example.com/2021/08/20/java/io/序列化/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.