序列化与反序列化知识

237 阅读10分钟

序列化与反序列化知识

  • 序列化与反序列化

    • 序列化:

      • 概述:将一个数据结构或者对象转成二进制串的过程

      • 应用场景:跨进程通信(通过网络进行传输)

      • 序列化方案:

        1. Serializable:java中的
        2. Parcelable:Android独有的
        3. 广义序列化:json,xml,protbuf……甚至是自己定义的协议
      • 如何选择合理的序列化方案

        1. 通用性:是否可以跨平台
        2. 健壮性(鲁棒性):容错
        3. 可读性/可调试性:序列化后的东西可读不
        4. 安全性等;
      • Serializable使用:

        1. 实现Serializable接口

          • 这个接口里面它是空的,只是相当于一个标识

          • 空接口如何实现序列化:

            • ObjectOutPut(输入输出流)
            • ObjectStreamClass:描述一个对象的结构
        2. 实现Externallizable接口:这个类是实现了Serializable接口的

          • 提供了两个方法:writeExternal(ObjectOutPut out)与readExternal(ObjectOutPut in)
    • 反序列化:

      • 将序列化过程中生成的二进制串转换成数据结构或者对象
    • 持久化:硬盘

      • 将数据结构或者对象存储起来,用的时候将其反序列化出来
  • Serializable具体使用

    • 代码思路:序列化

      1. 实现Serializable接口:

         static class User implements Serializable 
        
      2. 编写工具类,借助了对象输出流,将对象转成了二进制

        核心就是ObjectOutputStream.writeObject(obj);

             synchronized public static boolean saveObject(Object obj, String path) {//持久化
                 if (obj == null) {
                     return false;
                 }
                 ObjectOutputStream oos = null;
                 try {
                     oos = new ObjectOutputStream(new FileOutputStream(path));// 创建序列化流对象
                     oos.writeObject(obj);
                     oos.close();
                     return true;
                 } catch (IOException e) {
                     e.printStackTrace();
                 } finally {
                     if (oos != null) {
                         try {
                             oos.close(); // 释放资源
                         } catch (IOException e) {
                             e.printStackTrace();
                         }
                     }
                 }
                 return false;
             }
        
        • 此时对象就存在本地(持久化,调用时将obj对象存入本地的path路径),可以看,但是可读性不高(这个通过二进制数据流写进去的)
    • 代码思路:反序列化

      1. 实现Serializable接口:

         static class User implements Serializable 
        
      2. 编写工具类:借助了对象读入流,将二进制转成对象

        核心:ObjectInputStream.readObject();

             synchronized public static <T> T readObject(String path) {
                 ObjectInputStream ojs = null;
                 try {
                     ojs = new ObjectInputStream(new FileInputStream(path));// 创建反序列化对象
                     return (T) ojs.readObject();// 还原对象
                 } catch (IOException | ClassNotFoundException e) {
                     e.printStackTrace();
                 } finally {
                     if(ojs!=null){
                         try {
                             ojs.close();// 释放资源
                         } catch (IOException e) {
                             e.printStackTrace();
                         }
                     }
                 }
                 return null;
             }
        
    • 细节:

      • java使用Serializable接口实现序列化时,需要对象I/O流进行辅助

  • 使用Externallizable接口实现序列化:

    • 代码展示:

          private static void ExternalableTest() {
               User user = new User("zero", 18);
               System.out.println("1: " + user);
               //将对象I/O流进行一次包装
               ByteArrayOutputStream out = new ByteArrayOutputStream();
               ObjectOutputStream oos = null;
               byte[] userData = null;
               try {
                   oos = new ObjectOutputStream(out);
                   oos.writeObject(user);
                   userData = out.toByteArray();
               } catch (IOException e) {
                   e.printStackTrace();
               }
       ​
       ​
               ObjectInputStream ois = null;
               try {
                   ois = new ObjectInputStream(new ByteArrayInputStream(userData));
                   user = (User)ois.readObject();
                   System.out.println("反序列化后 2: " + user);
               } catch (Exception e) {
                   e.printStackTrace();
               }
           }
      
    • 实现细节:

      1. 需要重写writeExternal()与readExternal()并传入指定对象同时需要指定所有的成员变量

                 @Override
                 public void writeExternal(ObjectOutput out) throws IOException {
                     out.writeObject(name);
                     out.writeInt(age);
                 }
         ​
                 @Override
                 public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
                   name = (String)in.readObject();
                   age = in.readInt();
                 }
        
        • 如果说在writeExternal()方法中少写了类属性但是读取的时候又是全部都读取了的,那么就会抛出异常:java.io.EOFException
        • 如果说在这两个方法中,对某一类属性均缺失,那么反序列化得到的对象的这个属性就是零值(对象初始化中的默认值)
        • 还有对类属性读取顺序,两个方法中要完全一致,不然要报错
      2. 需要实现类的无参构造即使实现了其他的构造函数:

        • 不然就报错:java.io.InvalidClassException(没有合法的构造函数)

          • 为什么?这个在源码中才知道
  • 关于序列化的面试题:

    1. 什么是SerialVersionUID,如果不定义这个,会发生什么?

      • 概述:通常是对象的哈希码,可以用工具看;

      • 作用:用来做对象的版本控制

      • 不定义的话,当修改或者添加类的任何字段,此时已经序列化的对象无法反序列化回去

        • 因为此时新类的UID,跟反序列化回来的UID不同
        • 抛出:java.io.InvalidClassExption(无效类异常)
      • 怎么指定SerialVersionUID?直接显示定义

          private static final long serialVersionUID = 2;
        
    2. 事先定义好了SerialVersionUID,在序列化之后,再次修改SerialVersionUID,发生什么?

      • 因为SerialVersionUID不同,反序列化失败,报错,抛出异常

      • 使用场景:

        • 客户端将服务器端的一个对象缓存起来,每次用的时候就掉缓存,当服务器端需要更新客户端的这个缓存对象;那么,修改SerialVersionUID就行了
    3. 序列化是,希望某些成员不要序列化,如何实现?还可以问trasient瞬态变量拿来干啥子的?

      • 不想被序列化,在声明的时候加上trasient就行了(反序列化后,这个字段为null,其实是零值)

         public transient String nickName;
        
    4. 如果类中的一个成员未实现序列化接口,会发生什么情况?

      • 对可序列化的对象进行序列化的时候,此时若类包含了不可序列化的对象的引用;

        • 在类里面搞一个自定义的类(不实现序列化接口)的对象,那么抛出异常
        • String类是实现了序列化接口的
    5. 如果类是可序列化(实现了接口)的,但是父类不是,则反序列化后从父类继承的实例变量的状态如何?

      • 就是去看这个父类中的成员变量还有没有值
      • 反序列的值是空的;其实可以解决这个问题(父类要实现无参构造,加入方法)
    6. 是否可以自定义序列化过程,或者是否可以覆盖Java中的默认序列化过程:可以的

      1. 自定义序列化:借助对象I/O流直接操作对象(private修饰的两个函数合适调用)

         private void writeObject(ObjectOutputStream out) throws IOException {//不是重写父类的方案
             out.defaultWriteObject();
             out.writeObject(getSex());
             out.writeInt(getId());
         }
         ​
         private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
             in.defaultReadObject();
             setSex((String)in.readObject());
             setId(in.readInt());
         }
        
    7. 假设子类的父类实现了可序列化接口,如何避免子类被序列化

      1. 子类继承了实现序列化接口的父类,那么子类就默认可序列化,

        • 不想这样干,实现相应的序列化方法,在调用这个方法的时候就抛异常

        • 代码:

           private void writeObject(java.io.ObjectOutputStream out) throws IOException {
               throw new NotSerializableException("Can not serialize this class");
           }
           ​
           private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException {
               throw new NotSerializableException("Can not serialize this class");
           }
           ​
           private void readObjectNoData() throws ObjectStreamException {
               throw new NotSerializableException("Can not serialize this class");
           }
          
    8. java序列化和反序列化过程中使用了哪些方法

      • 概述:实现的序列化接口只是一个标识,核心还是在对象I/O流

      • 可以对对象I/O流进行封装(内部就可以使用很多其他的东西)

  • 序列化流程(源码)

    • 序列化:

      • 调度流程:

        1. objectOutPutStream 实例.writeObject(待序列化类的实例);

           out.writeObject(getSex());
          
        2. 进入public final void writeObject(Object obj) 函数-

           this.writeObject0(obj, false);
          
        3. 进入private void writeObject0(Object obj, boolean unshared)函数

          • 参数为基本类型就直接写,是类就走具体的方法

              if (obj instanceof String) {
                   this.writeString((String)obj, unshared);
                   return;
              } else if (cl.isArray()) {
                   this.writeArray(obj, desc, unshared);
                   return;
                  ……
            
          • 细节:对枚举类型由特殊处理,存入枚举类型的name

             private void writeEnum(Enum<?> en, ObjectStreamClass desc, boolean unshared) throws IOException {
                 this.bout.writeByte(126);
                 ObjectStreamClass sdesc = desc.getSuperDesc();
                 this.writeClassDesc(sdesc.forClass() == Enum.class ? desc : sdesc, false);
                 this.handles.assign(unshared ? null : en);
                 this.writeString(en.name(), false);
             }
            
          • 细节:writeOrdinaryObject(根据对象实现的序列化接口的不同,进行分发)

             if (desc.isExternalizable() && !desc.isProxy()) {             this.writeExternalData((Externalizable)obj);
                         } else {
                             this.writeSerialData(obj, desc);
                         }
            
          • 细节:writeOrdinaryObject

            • 拿到对象描述符(或者地址)去写,

               this.bout.writeByte(115);
               this.writeClassDesc(desc, false);
                xxx
               if (desc.isExternalizable() && !desc.isProxy()) {                       this.writeExternalData((Externalizable)obj);
               } else {
                   this.writeSerialData(obj, desc);
               }
              
        4. 进入private void writeClass(Class<?> cl, boolean unshared)函数

          • 通过字节形式进行读取,传入了一个对象描述符
           this.bout.writeByte(118);//通过字节形式进行读取,传入了一个对象描述符
           this.writeClassDesc(ObjectStreamClass.lookup(cl, true), false);
           this.handles.assign(unshared ? null : cl);
          
        5. 进入private void writeClassDesc(ObjectStreamClass desc, boolean unshared)函数

          • 开始写入对象

             if (!unshared && (handle = this.handles.lookup(desc)) != -1) {
                 this.writeHandle(handle);
             } else if (desc.isProxy()) {
                 this.writeProxyDesc(desc, unshared);
             } else {
                 this.writeNonProxyDesc(desc, unshared);
             }
            
        6. 进入函数private void writeProxyDesc(ObjectStreamClass desc, boolean unshared)

          • 写对象

             this.bout.setBlockDataMode(false);
             this.bout.writeByte(120);
             this.writeClassDesc(desc.getSuperDesc(), false);
            
    • 序列化中private方法,又不是重写了父类的,这个是在哪里调用的?

      • 概述:通过反射调用的

      • 证明:有一个hashWriteObjectMethod方法

        • 存在ObjectStreamClass类:描述类待序列化类的结构
        • 在这个类里面读取类结构,看有没有writeObject方法,有就存起来
        • 然后会调用hashWriteObjectMethod,有那么就invoke反射调用这个方法
    • 为什么每次调用都要求有一个无参构造函数?

      1. 当去read一个对象的时候,调用readObject(Object.class)
      2. 调用readObject0(type,unshared:false)
      3. 调用readOrdinaryObject(unshared)
      4. 调用newInstance:默认去调用待序列化类的无参构造函数
    • java序列化对枚举类型的处理:

      • 概述:

        • 在序列化过程中,只存储枚举类型的引用和常量的名称(en.name)
        • 在反序列化过程中,在运行时环境中查找已经存在的枚举对象
      • 代码检验

        1. 定义枚举类型

        2. 打印序列化后与反序列化后的hashCode,发现是一样的(相同的对象引用时完全相同的)为什么会这样呢?

          枚举对象的读取流程

          1. readEnum时,拿到枚举对象的名字
          2. 调用Enum.valueOf((Class)C1,name)
          3. 调用enumConstantDirectory().get(name):通过字典直接拿到枚举对象

          将枚举对象序列化时的流程

          1. 在序列化对象(转成二进制),先存储从哪里开始

             bout.writeByte(TC_ENUM)//里面是对象标记
            
          2. 发现这个对象是枚举类型,那么就只存一个name

             writeString(en.name(),false)
            
          3. 读取的时候,还是去找枚举字典

      • 在序列化时使用枚举的好处

        • 使用枚举类型单例,可以避免单例在序列化时失效
      • 在保证单例序列化与反序列化后,单例仍然存在

        • 代码展示:这种是可以通过反射进行破坏的

           class SingleTon implements Serializable{
               public static SingleTon INSTANCE = new SingleTon();
               
               private Singleton();
               
               private Object readResolve(){
                   return INSTANCE;
               }
           }
          
        • 解决反射破坏问题:使用synchronized(SingleTon.class)包裹构造函数

           private SingleTon(){
               synchronized(SingleTon.class){
                   if(flag == false){
                       flag = true
                   }else{
                       throw new RuntimeException("破坏单例了")
                   }
               }
           }
          
        • 补充:readResolve方法

          当调用readOrdinaryObject时

          1. 检查是否实现了 readResolve方法,最先检测

             desc.hasResolveMethod()
            
          2. 如果说实现了那就去调用,那么就过滤掉了系统的其他方法

    • 流程:

      • 示意图:

        image-20220218192328278

  • Parcelable

    • 概述:Android特有序列化方案,基于Binder实现进程间通信

    • 简单使用:一般是自动生成代码

      1. 实现Parceable接口

      2. 重写方法:

        • describeContents(一般返回0,特殊处理返回1)
        • writeToParcelable(调用write具体类型写进去):如何进行序列化
      3. 读取的时候需要有构造,需要去new对象

         public User(Parcel in){
             name = in.readString();
             age = in.readInt();
         }
        
    • 构造中的读取调用流程:

      • readString--->nativeReadString--->framwork层

        • write方法会调用到writeAligned(FrameWork层)
        • read方法会调用到readAligned(FrameWork层)
    • Serializable与Paecelable的区别

      • 示意图:

        image-20220218204556517

      • 为什么是1M,修改内核是4M?

        • Binder通信机制规定了,一个进程最多就1M,要是改内核就是4M
      • Serializable 产生内存碎片

        • 在创建对象的时候,大量使用了反射操作
  • 面试题

    • 反序列化后对象,需要调用构造函数重新构造吗?

      • 不会(序列化会调用构造函数),反序列实际上是读取二进制数据,拼接成Object,然后进行强制类型转化
      • 为什么会产生内存碎片,为什么效率低:在反序列化时在对成员变量处理的时候,就会调用无参构造函数
    • 序列化和反序列化后的对象是什么关系(是==还是equal,是深拷贝还是浅拷贝)

      1. 就相当于是深拷贝,对象前后的地址是不同的;但是枚举例外
    • Android里面为什么要设计出Bundle而不直接使用Map结构

      • Bundle内部使用了ArrayMap(比HashMap省空间)并且Bundle主要用于传输少量数据

         static{
             xxx
             EMPTY = new Bundle();
             EMPTY.mMap = ArrayMap.EMPTY();
         }
        
    • 为何Intent不能在组件之间直接传递对象而要通过序列化机制?

      • 这个牵涉到AMS与Bundle
      • 因为Activity通信是需要跟AMS进行交互的(静态代理较多),而AMS与APP进程不是同一个进程,所以需要跨进程通信;
    • 序列化与持久化的关系与区别

      • 序列化:跨进程通信;持久化:存储数据;