美文网首页IT@程序员猿媛Java
序列化机制——Serializable工作原理

序列化机制——Serializable工作原理

作者: 未见哥哥 | 来源:发表于2019-05-11 18:16 被阅读3次
    序列化和反序列化

    序列化与反序列化的概念

    从广义上讲,数据序列化就是将数据结构或者是对象转换成我们可以存储或者传输的数据格式的一个过程,在序列化的过程中,数据结构或者对象将其状态信息写入到临时或者持久性的存储区中,而在对应的反序列化过程中,则可以说是生成的数据被还原成数据结构或对象的过程。

    在对象序列化和反序列化角度来看,Java 提供了 Serializable 接口,而 Android 提供特有的 Parcelable 接口。从广义的角度来看,Json ,XML,SharedPreference,SQLite 等都是属于序列化的范畴。

    序列化与反序列化的目的

    序列化:得到的字节序列可以方便在网络上传输或者持久化——编码。

    反序列化:主要从网络/磁盘读取读取字节序列然后转化为对象或者数据结构。——解码

    几种常见的序列化和反序列化协议

    几种常见的序列化和反序列化协议

    XML&SOAP

    XML 是一种常用的序列化和反序列化协议,具有跨机器,跨语言等优点,SOAP(Simple Object Access protocol) 是一种被广泛应用的,基于 XML 为序列化和反序列化协议的结构化消息传递协议

    JSON(Javascript Object Notation)

    Protobuf

    序列化方案

    Serializable 接口

    Serializable 接口

    是 Java 提供的序列化接口,它是一个空接口:

    public interface Serializable {
    }
    

    Serializable 用来标识当前类可以被 ObjectOutputStream 序列化,以及被 ObjectInputStream 反序列化。

    Serializable 接口的基本使用

    通过 ObjectOutputStream 将需要序列化数据写入到流中,因为 Java IO 是一种装饰者模式,因此可以通过 ObjectOutStream 包装 FileOutStream 将数据写入到文件中或者包装 ByteArrayOutStream 将数据写入到内存中。同理,可以通过 ObjectInputStream 将数据从磁盘 FileInputStream 或者内存 ByteArrayInputStream 读取出来然后转化为指定的对象即可。

    Serializable 接口的特点

    Serializable 接口的特点
    1. 序列化类的属性没有实现 Serializable 那么在序列化就会报错

    具体可以跟进 ObjectOutputStream#writeObject() 源码查看具体原因:

    Exception in thread "main" java.io.NotSerializableException: com.example.seriable.Color
    
    public class Student implements Serializable {
      private String name;
      private int age;
      /**
      * Color 类也是需要实现序列化接口的。
      */
      private Color color;//这里如果没有实现序列化接口,那么在 Student 对象序列化时将会报错
    }
    
    2. 在反序列化过程中,它的父类如果没有实现序列化接口,那么将需要提供无参构造函数来重新创建对象。
    • Animal 是父类,它没有实现 Serilizable 接口
    public class Animal {
        private String color;
    
        public Animal() {//没有无参构造将会报错
            System.out.println("调用 Animal 无参构造");
        }
    
        public Animal(String color) {
            this.color = color;
    
                System.out.println("调用 Animal 有 color 参数的构造");
        }
    
        @Override
        public String toString() {
            return "Animal{" +
                    "color='" + color + '\'' +
                    '}';
        }
    }
    
    • BlackCat 是 Animal 的子类
    public class BlackCat extends Animal implements Serializable {
        private static final long serialVersionUID = 1L;
        private String name;
    
        public BlackCat() {
            super();
            System.out.println("调用黑猫的无参构造");
        }
    
        public BlackCat(String color, String name) {
            super(color);
            this.name = name;
            System.out.println("调用黑猫有 color 参数的构造");
        }
    
        @Override
        public String toString() {
            return "BlackCat{" +
                    "name='" + name + '\'' +super.toString() +'\'' +
                    '}';
        }
    }
    
    • SuperMain 测试类
    public class SuperMain {
        private static final String FILE_PATH = "./super.bin";
    
        public static void main(String[] args) throws Exception {
            serializeAnimal();
            deserializeAnimal();
        }
    
        private static void serializeAnimal() throws Exception {
            BlackCat black = new BlackCat("black", "我是黑猫");
            System.out.println("序列化前:"+black.toString());
            System.out.println("=================开始序列化================");
            ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(FILE_PATH));
            oos.writeObject(black);
            oos.flush();
            oos.close();
        }
    
        private static void deserializeAnimal() throws Exception {
            System.out.println("=================开始反序列化================");
            ObjectInputStream ois = new ObjectInputStream(new FileInputStream(FILE_PATH));
            BlackCat black = (BlackCat) ois.readObject();
            ois.close();
            System.out.println(black);
        }
    }
    
    • 输出结果
    调用 Animal 有 color 参数的构造
    调用黑猫有 color 参数的构造
    序列化前:BlackCat{name='我是黑猫'Animal{color='black'}'}
    =================开始序列化================
    =================开始反序列化================
    调用 Animal 无参构造
    BlackCat{name='我是黑猫'Animal{color='null'}'}
    

    从上面的执行结果来看,如果要序列化的对象的父类 Animal 没有实现序列化接口,那么在反序列化时是会调用对应的无参构造方法的,这样做的目的是重新初始化父类的属性,例如 Animal 因为没有实现序列化接口,因此对应的 color 属性就不会被序列化,因此反序列得到的 color 值就为 null。

    3. 一个实现 Serializable 接口的子类也是可以被序列化的。
    4. 静态成员变量是不能被序列化

    序列化是针对对象属性的,而静态成员变量是属于类的。

    5. transient 标识的对象成员变量不参与序列化

    在下面这个栗子中,MyLIst 有一个属性 arr 数组,初始化情况大小有 100 长度,它适用于存放 MyList 的数据的,但是在实际序列化时如果让 arr 属性参与序列化的话,那么 100 长度的数据都会被序列化下来,这明显是不可理的,所以这里就要自定义序列化过程拉,具体的做法是写以下两个 private 方法:

    private void writeObject(java.io.ObjectOutputStream s)throws java.io.IOException
    private void readObject(java.io.ObjectInputStream s)throws java.io.IOException, ClassNotFoundException
    

    从这两个方法的名字就可以看出分别是序列化写入数据和反序列化读取数据用的,那么这两个方法是在哪里使用呢?其实在序列化过程中会通过反射调用的,具体下面会分析这个过程哦。

    现在来看看这个 transient 应用:

    public class MyList implements Serializable {
    
        private String name;
    
    
        /*
        transient 表示该成员 arr 不需要被序列化
         */
        private transient Object[] arr;
    
        public MyList() {
        }
    
        public MyList(String name) {
            this.name = name;
            this.arr = new Object[100];
            /*
            给前面30个元素进行初始化
             */
            for (int i = 0; i < 30; i++) {
                this.arr[i] = i;
            }
        }
    
        @Override
        public String toString() {
            return "MyList{" +
                    "name='" + name + '\'' +
                    ", arr=" + Arrays.toString(arr) +
                    '}';
        }
    
    
        //-------------------------- 自定义序列化反序列化 arr 元素 ------------------
    
        /**
         * Save the state of the <tt>ArrayList</tt> instance to a stream (that
         * is, serialize it).
         *
         * @serialData The length of the array backing the <tt>ArrayList</tt>
         * instance is emitted (int), followed by all of its elements
         * (each an <tt>Object</tt>) in the proper order.
         */
        private void writeObject(java.io.ObjectOutputStream s)
                throws java.io.IOException {
            //执行 JVM 默认的序列化操作
            s.defaultWriteObject();
    
    
            //手动序列化 arr  前面30个元素
            for (int i = 0; i < 30; i++) {
                s.writeObject(arr[i]);
            }
        }
    
        /**
         * Reconstitute the <tt>ArrayList</tt> instance from a stream (that is,
         * deserialize it).
         */
        private void readObject(java.io.ObjectInputStream s)
                throws java.io.IOException, ClassNotFoundException {
    
            s.defaultReadObject();
            arr = new Object[30];
    
            // Read in all elements in the proper order.
            for (int i = 0; i < 30; i++) {
                arr[i] = s.readObject();
            }
        }
    }
    
    
    • 测试
    public class TransientMain {
        private static final String FILE_PATH = "./transient.bin";
        public static void main(String[] args) throws Exception {
            serializeMyList();
    
            deserializeMyList();
        }
    
        private static void serializeMyList() throws Exception {
            System.out.println("序列化...");
            MyList myList = new MyList("ArrayList");
            ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(FILE_PATH));
            oos.writeObject(myList);
            oos.flush();
            oos.close();
        }
    
        /*
        1.如果 private  Object[] arr; 没有使用 transient ,那么整个数组都会被保存,而不是保存实际存储的数据
        输出结果:MyList{name='ArrayList', arr=[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null]}
        2.private transient Object[] arr;设置了 transient,表示 arr 元素不进行序列化
        输出结果:MyList{name='ArrayList', arr=null}
        3.参考 ArrayList 处理内部的 transient Object[] elementData; 数组是通过 writeObject 和 readObject 实现的
        我们的 MyList 内部也可以借鉴这种方式实现transient元素的手动序列化和反序列化。
         */
        private static void deserializeMyList() throws Exception {
            System.out.println("反序列化...");
            ObjectInputStream ois = new ObjectInputStream(new FileInputStream(FILE_PATH));
            MyList myList = (MyList) ois.readObject();
            ois.close();
            System.out.println(myList);
        }
    }
    
    
    • 测试输出结果
    序列化...
    writeObject...
    反序列化...
    readObject...
    MyList{name='ArrayList', arr=[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29]}
    

    serialVersionUID与兼容性问题

    serialVersionUID与兼容性问题

    这个 serialVersionUID 是什么以及它的作用是什么?

    这个值是用于确保类序列化与反序列化的兼容性问题的,如果序列化和反序列化过程中这两个值不一样,那么将导致序列化失败,具体可以看下面的 serialVersionUID 兼容性问题。

    如何生成这个 serialVersionUID呢?

    1. 使用 AS plugin 插件就可以生成
    2. 在JDK中,可以利用JDK的bin目录下的f.exe工具产生这个serialVersionUID,对于Student.class,执行命令:serialver com.example.seriable.Student
    ➜  classes git:(master) ✗ /Library/Java/JavaVirtualMachines/jdk1.8.0_111.jdk/Contents/Home/bin/serialver com.example.seriable.Student 
    com.example.seriable.Student:    private static final long serialVersionUID = -6840182814363029482L;//这个就是工具生成的 SerialVersionUID 值了
    

    使用 AS plugin 的方式应该底层也是使用到这个 JDK 工具去生成的 SerialVersionUID 值,测试结果来看这两个生成的值是一样的。

    serialVersionUID 的兼容性问题是什么?

    具体的兼容性问题如下:

    不兼容报错
    java.io.InvalidClassException: com.example.seriable.Student; local class incompatible: stream classdesc
    serialVersionUID = -926212341182608815, local class serialVersionUID = -6840182814363029482
    序列化时使用的 serialVersionUID = -926212341182608815L,如果期间属性被修改了,如果 serialVersionUID 发生改变 -6840182814363029482 ,那么
    反序列化时就会出现类不兼容问题。
    serialVersionUID 发生改变有三种情况:
        1. 手动去修改导致当前的 serialVersionUID 与序列化前的不一样。
        2. 根本就没有手动去写这个 serialVersionUID 常量,那么 JVM 内部会根据类结构去计算得到这个 serialVersionUID 值,这种也是会导致 serialVersionUID 发生变化。
        3. 假如类结构没有发生改变,并且没有显示去写 serialVersionUID ,但是反序列和序列化操作的虚拟机不一样也可能导致计算出来的 serialVersionUID 不一样。
    

    JVM 规范强烈建议我们手动声明一个版本号,这个数字可以是随机的,只要固定不变就可以。同时最好是 private 和 final 的,尽量保证不变。

    Externalizable 接口

    Serializable 接口内部序列化是 JVM 自动实现的,如果我们想自定义序列化过程,就可以使用以上这个接口来实现,它内部提供两个接口方法:

    public interface Externalizable extends Serializable {
        //将要序列化的对象属性通过 var1.wrietXxx() 写入到序列化流中
        void writeExternal(ObjectOutput var1) throws IOException;
            //将要反序列化的对象属性通过 var1.readXxx() 读出来
        void readExternal(ObjectInput var1) throws IOException, ClassNotFoundException;
    }
    
    • Externalizable 的使用
    public class Person implements Externalizable {
    
        private static final long serialVersionUID = -7424420983806112577L;
        private String name;
        private int age;
        /*
        实现了Externalizable这个接口需要提供无参构造,在反序列化时会检测
         */
        public Person() {
            System.out.println("Person: empty");
        }
    
        public Person(String name, int age) {
            this.name = name;
            this.age = age;
        }
    
        @Override
        public void writeExternal(ObjectOutput out) throws IOException {
            System.out.println("person writeExternal...");
            out.writeObject(name);
            out.writeInt(age);
        }
    
        @Override
        public void readExternal(ObjectInput in) throws ClassNotFoundException, IOException {
            System.out.println("person readExternal...");
    
            name = (String) in.readObject();
            age = in.readInt();
        }
    
        @Override
        public String toString() {
            return "Person{" +
                    "name='" + name + '\'' +
                    ", age=" + age +
                    '}';
        }
    }
    
    
    • 测试 Person 对象的序列化和反序列化
    public class ExternalizableMain {
        private static final String FILE_PATH = "../person.bin";
    
        public static void main(String[] args) throws IOException, ClassNotFoundException {
    
            Person person = new Person("zhangsan", 15);
            System.out.println(person.toString());
            serializable(person, FILE_PATH);
            System.out.println("============反序列化=============");
            person = (Person) deserializable(FILE_PATH);
            System.out.println(person.toString());
        }
    
        private static void serializable(Object o, String path) throws IOException {
            FileOutputStream boas = new FileOutputStream(path);
            ObjectOutputStream oos = new ObjectOutputStream(boas);
            oos.writeObject(o);
            oos.close();
            boas.close();
        }
    
        private static Object deserializable(String path) throws IOException, ClassNotFoundException {
            ObjectInputStream bis = new ObjectInputStream(new FileInputStream(path));
            Object obj = bis.readObject();
            return obj;
        }
    }
    
    • 测试输出结果:
    Person{name='zhangsan', age=15}
    person writeExternal...
    ============反序列化=============
    Person: empty
    person readExternal...
    Person{name='zhangsan', age=15}
    

    Java 的序列化步骤与数据结构分析

    序列化算法一般会按步骤做如下事情:

    • 将对象实例相关的类元数据输出。
    • 递归地输出类的超类描述直到不再有超类。
    • 类元数据完了以后,开始从最顶层的超类开始输出对象实例的实际数据值。
    • 从上至下递归输出实例的数据

    也许你看上面这几个步骤会有点懵逼,不过实际的序列化过程就是按照上面的步骤进行的,看完这个就开始 writeObject 和 readObject 源码解读,而 writeObejct 的过程就是上面的4个步骤。

    readObject/writeObject原理分析

    writeObject 原理分析

    • ObjectOutputStream 构造函数
    public ObjectOutputStream(OutputStream out) throws IOException {
        verifySubclass();
        bout = new BlockDataOutputStream(out);//①
        handles = new HandleTable(10, (float) 3.00);
        subs = new ReplaceTable(10, (float) 3.00);
        enableOverride = false;//②
        writeStreamHeader();//③
        bout.setBlockDataMode(true);
        if (extendedDebugInfo) {
            debugInfoStack = new DebugTraceInfoStack();
        } else {
            debugInfoStack = null;
        }
    }
    

    ①bout:用于写入一些类元数据还有对象中基本数据类型的值,在下面会分析。

    ②enableOverride :false 表示不支持重写序列化过程,如果为 true ,那么需要重写 writeObjectOverride 方法。这个一般不用管它。

    ③writeStreamHeader() 写入头信息,具体看下面分析。

    • ObjectOutputStream#writeStreamHeader()
    protected void writeStreamHeader() throws IOException {
        bout.writeShort(STREAM_MAGIC);//①
        bout.writeShort(STREAM_VERSION);//②
    }
    

    ①STREAM_MAGIC 声明使用了序列化协议,bout 就是一个流,将对应的头数据写入该流中

    ②STREAM_VERSION 指定序列化协议版本

    • ObjectOUtStream#writeObject(obj);

    上面是 ObjectOutStream 构造中做的事,下面来看看具体 writeObject 方法内部做了什么事?

    public final void writeObject(Object obj) throws IOException {
        if (enableOverride) {//一般不会走这里,因为在 ObjectOutputStream 构造设置为 false 了
            writeObjectOverride(obj);
            return;
        }
        try {//代码会执行这里
            writeObject0(obj, false);
        } catch (IOException ex) {
            ...
        }
    }
    
    • ObjectOutStream#writeObject0()
    private void writeObject0(Object obj, boolean unshared)
        throws IOException
    {
        ...
        try {
         
            Object orig = obj;
            Class<?> cl = obj.getClass();
            ObjectStreamClass desc;
           
            //①
            desc = ObjectStreamClass.lookup(cl, true);
            ...
            //②
            if (obj instanceof Class) {
                writeClass((Class) obj, unshared);
            } else if (obj instanceof ObjectStreamClass) {
                writeClassDesc((ObjectStreamClass) obj, unshared);
            // END Android-changed:  Make Class and ObjectStreamClass replaceable.
            } else 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) {
                writeOrdinaryObject(obj, desc, unshared);
            } else {
                    //③
                if (extendedDebugInfo) {
                    throw new NotSerializableException(
                        cl.getName() + "\n" + debugInfoStack.toString());
                } else {
                    throw new NotSerializableException(cl.getName());
                }
            }
        } 
        ...
    }
    

    ① lookup 函数用于查找当前类的 ObjectStreamClass ,它是用于描述一个类的结构信息的,通过它就可以获取对象及其对象属性的相关信息,并且它内部持有该对象的父类的 ObjectStreamClass 实例。其内部大量使用了反射,读者可以去看看这个类的源码。

    下面看看它的构造函数

    private ObjectStreamClass(final Class<?> cl) {
        this.cl = cl;
        name = cl.getName();
        isProxy = Proxy.isProxyClass(cl);
        isEnum = Enum.class.isAssignableFrom(cl);
        serializable = Serializable.class.isAssignableFrom(cl);
        externalizable = Externalizable.class.isAssignableFrom(cl);
        Class<?> superCl = cl.getSuperclass();
        //superDesc 表示需要序列化对象的父类的 ObjectStreamClass,如果为空,则调用 lookUp 查找
        superDesc = (superCl != null) ? lookup(superCl, false) : null;
            //localDesc 表示自己
        localDesc = this;
            ...
    }
    

    ② 根据 obj 的类型去执行序列化操作,如果不符合序列化要求,那么会③位置抛出 NotSerializableException 异常。

    在上面描述过,如果一个需要序列化的对象的某个属性没有实现序列化接口,那么就会此处抛出异常。读者可以自行验证。

    • ObjectOutputStream#writeOrdinaryObject
    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);
            }
        } finally {
            if (extendedDebugInfo) {
                debugInfoStack.pop();
            }
        }
    }
    

    ①写入类的元数据,TC_OBJECT. 声明这是一个新的对象,如果写入的是一个 String 类型的数据,那么就需要 TC_STRING 这个标识。

    ②writeClassDesc 方法主要作用就是自上而下(从父类写到子类,注意只会遍历那些实现了序列化接口的类)写入描述信息。该方法内部会不断的递归调用,我们只需要关系这个方法是写入描述信息就好了,读者可以查阅一下源码。

    从这里可以知道,序列化过程需要额外的写入很多数据,例如描述信息,类数据等,因此序列化后占用的空间肯定会更大。

    ③ desc.isExternalizable() 判断需要序列化的对象是否实现了 Externalizable 接口,这个在上面已经演示过怎么使用的,在序列化过程就是在这个地方进行判断的。如果有,那么序列化的过程就会由程序员自己控制了哦,writeExternalData 方法会回调,在这里就可以愉快地编写需要序列化的数据拉。

    ④ writeSerialData 在没有实现 Externalizable 接口时,就执行这个方法

    • ObjectOutputstream#writeSerialData
    private void writeSerialData(Object obj, ObjectStreamClass desc)
        throws IOException
    {
            //① 
        ObjectStreamClass.ClassDataSlot[] slots = desc.getClassDataLayout();
        for (int i = 0; i < slots.length; i++) {
        
            ObjectStreamClass slotDesc = slots[i].desc;
            
            if (slotDesc.hasWriteObjectMethod()) {//②
                PutFieldImpl oldPut = curPut;
                curPut = null;
                SerialCallbackContext oldContext = curContext;
                if (extendedDebugInfo) {
                    debugInfoStack.push(
                        "custom writeObject data (class \"" +
                        slotDesc.getName() + "\")");
                }
                try {
                    curContext = new SerialCallbackContext(obj, slotDesc);
                    bout.setBlockDataMode(true);
                    slotDesc.invokeWriteObject(obj, this);
                    bout.setBlockDataMode(false);
                    bout.writeByte(TC_ENDBLOCKDATA);
                } finally {
                    curContext.setUsed();
                    curContext = oldContext;
                    if (extendedDebugInfo) {
                        debugInfoStack.pop();
                    }
                }
                curPut = oldPut;
            } else {
                defaultWriteFields(obj, slotDesc);//③
            }
        }
    }
    

    ① desc.getClassDataLayout 会返回 ObjectStreamClass.ClassDataSlot[] ,我们来看看 ClassDataSlot 类,可以看到它是封装了 ObjectStreamClass 而已,所以我们就简单的认为 ① 这一步就是用于返回序列化对象及其父类的 ClassDataSlot[] 数组,我们可以从 ClassDataSlot 中获取对应 ObjectStreamClass 描述信息。

    static class ClassDataSlot {
        /** class descriptor "occupying" this slot */
        final ObjectStreamClass desc;
        /** true if serialized form includes data for this slot's descriptor */
        final boolean hasData;
        ClassDataSlot(ObjectStreamClass desc, boolean hasData) {
            this.desc = desc;
            this.hasData = hasData;
        }
    }
    

    ② 开始遍历返回的数组,slotDesc 这个我们就简单将其看成对一个对象的描述吧。hasWriteObjectMethod 表示的是什么呢?这个其实就是你要序列化这个对象是否有 writeObject 这个 private 方法,注意哦,这个方法并不是任何接口的方法,而是我们手动写的,读者可以参考 ArrayList 代码,它内部就有这个方法。

    那么这个方法的作用是什么呢?这个方法我们在上面也演示过具体的使用,它就是用于自定义序列化过程的,读者可以返回到上面看看如果使用这个 writeObject 实现自定义序列化过程的。注意:其实这个过程不像实现 Externalizable 接口那样,自己完全去自定义序列化数据。

    private void writeObject(java.io.ObjectOutputStream s)
            throws java.io.IOException {
        //执行 JVM 默认的序列化操作
        s.defaultWriteObject();
        //手动序列化 arr  前面30个元素
        for (int i = 0; i < 30; i++) {
            s.writeObject(arr[i]);
        }
    }
    

    ③ defaultWriteFields 这个方法就是 JVM 自动帮我们序列化了,

    private void defaultWriteFields(Object obj, ObjectStreamClass desc)
        throws IOException
    {
        Class<?> cl = desc.forClass();
    
        desc.checkDefaultSerialize();
        int primDataSize = desc.getPrimDataSize();
        if (primVals == null || primVals.length < primDataSize) {
            primVals = new byte[primDataSize];
        }
        desc.getPrimFieldValues(obj, primVals);
        //①
        bout.write(primVals, 0, primDataSize, false);
        ObjectStreamField[] fields = desc.getFields(false);
        Object[] objVals = new Object[desc.getNumObjFields()];
        int numPrimFields = fields.length - objVals.length;
        desc.getObjFieldValues(obj, objVals);
        
        //②
        for (int i = 0; i < objVals.length; i++) {
            if (extendedDebugInfo) {
                debugInfoStack.push(
                    "field (class \"" + desc.getName() + "\", name: \"" +
                    fields[numPrimFields + i].getName() + "\", type: \"" +
                    fields[numPrimFields + i].getType() + "\")");
            }
            try {
                writeObject0(objVals[i],
                             fields[numPrimFields + i].isUnshared());
            } finally {
                if (extendedDebugInfo) {
                    debugInfoStack.pop();
                }
            }
        }
    }
    

    这个方法主要分为以下两步

    • ① 写入基本数据类型的数据
    • ②写入引用数据类型的数据,这里最终又调用到了 writeObject0() 方法,读者可以返回到上面去看看具体的实现。

    好了,Serialzable 序列化的源码分析就完成了。

    readObject 原理分析

    从流中读取类的描述信息 ObjectStreamClass 实例,通过这个对象就可以创建出序列化的对象。

    ObjectStreamClass desc = readClassDesc(false);
    ...
      Object obj;
    try {
        //创建对应反序列化的对象
        obj = desc.isInstantiable() ? desc.newInstance() : null;
    } catch (Exception ex) {
        throw (IOException) new InvalidClassException(
            desc.forClass().getName(),
            "unable to create instance").initCause(ex);
    }
    

    读取该对象及其对象的父类的 ObjectStreamClass信息

    ObjectStreamClass.ClassDataSlot[] slots = desc.getClassDataLayout();
    

    然后遍历得到每一个 ObjectStreamClass 对象,将对应的属性值赋值给需要反序列化的对象。

    private void defaultReadFields(Object obj, ObjectStreamClass desc)
        throws IOException
    {
        Class<?> cl = desc.forClass();
        if (cl != null && obj != null && !cl.isInstance(obj)) {
            throw new ClassCastException();
        }
        int primDataSize = desc.getPrimDataSize();
        if (primVals == null || primVals.length < primDataSize) {
            primVals = new byte[primDataSize];
        }
        bin.readFully(primVals, 0, primDataSize, false);
        if (obj != null) {
            desc.setPrimFieldValues(obj, primVals);
        }
        int objHandle = passHandle;
        //从 ObjectStreamClass 中得到对象的所有 Field 信息
        ObjectStreamField[] fields = desc.getFields(false);
        Object[] objVals = new Object[desc.getNumObjFields()];
        int numPrimFields = fields.length - objVals.length;
        for (int i = 0; i < objVals.length; i++) {
            ObjectStreamField f = fields[numPrimFields + i];
            objVals[i] = readObject0(f.isUnshared());
            if (f.getField() != null) {
                handles.markDependency(objHandle, passHandle);
            }
        }
        if (obj != null) {
                //将数据保存到对象中去
            desc.setObjFieldValues(obj, objVals);
        }
        passHandle = objHandle;
    }
    

    好了,以上就是基本的反序列化 readObject 的过程,这个过程基本是跟 writeObject 差不多,因此简单的列举了关键步骤而已。

    Serializable 需要注意的坑

    Serializable 需要注意的坑

    多引用写入

    //有 bug 版本
    //SerializableTest.java
    private static void serializeBug() throws Exception {
        System.out.println("序列化...");
        Student student = new Student("六号表哥", 25);
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(FILE_PATH));
        oos.writeObject(student);
        //修改属性再次写入
        student.setName("王武");
        oos.writeObject(student);
        oos.flush();
        oos.close();
    }
    
    //SerializableTest.java
    private static void deserialize() throws Exception {
        System.out.println("反序列化...");
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(FILE_PATH));
        Student student = (Student) ois.readObject();
        Student student2 = (Student) ois.readObject();
        ois.close();
        System.out.println(student);
        System.out.println(student2);
    }
    

    测试输出结果

    从输出结果来看,name 属性并没有发生改变

    序列化...
    反序列化...
    Student{name='六号表哥', age=25',color = null}
    Student{name='六号表哥', age=25',color = null}
    

    使用以下两种方法来修复这个问题:

    //bugfix 版本
    private static void serializeBugFix() throws Exception {
        System.out.println("序列化...");
        Student student = new Student("六号表哥", 25);
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(FILE_PATH));
        oos.writeObject(student);
        //方式一
        oos.reset();
        student.setName("王武");
        oos.writeObject(student);
        //方式二
        //oos.writeUnshared(student);
        oos.close();
    }
    
    private static void deserialize() throws Exception {
        System.out.println("反序列化...");
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(FILE_PATH));
        Student student = (Student) ois.readObject();
        Student student2 = (Student) ois.readObject();
        ois.close();
        System.out.println(student);
        System.out.println(student2);
    }
    

    输出结果:

    序列化...
    反序列化...
    Student{name='六号表哥', age=25',color = null}
    Student{name='王武', age=25',color = null}
    

    序列化的属性如果没有实现序列化接口会在序列化时会报错

    public class Student implements Serializable {
      private String name;
      private int age;
      /**
      * Color 类也是需要实现序列化接口的。
      */
      private Color color;//这里如果没有实现序列化接口,那么在 Student 对象序列化时将会报错
    }
    
    Exception in thread "main" java.io.NotSerializableException: com.example._super.Color
        at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1184)
        at java.io.ObjectOutputStream.defaultWriteFields(ObjectOutputStream.java:1548)
        at java.io.ObjectOutputStream.writeSerialData(ObjectOutputStream.java:1509)
        at java.io.ObjectOutputStream.writeOrdinaryObject(ObjectOutputStream.java:1432)
        at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1178)
        at java.io.ObjectOutputStream.writeObject(ObjectOutputStream.java:348)
        at com.example.seriable.SerializableTest.serialize(SerializableTest.java:43)
        at com.example.seriable.SerializableTest.main(SerializableTest.java:18)
    
    

    枚举序列化问题

    下图是 ObjectOutputStream 对枚举的序列化和反序列化的描述,大概的意思是,枚举的的序列化和反序列化和普通的对象不一样,它只会序列化枚举的 name 值,而反序列化是根据得到的 name 值去调用 enum 的 valueOf(name)得到枚举对象。还有一点不同的是枚举的序列化和反序列化过程是不能被定制的,因此类似于 readObject,writeObject,writeExternal 等方法是无效的,并且对应的 serialVersionUID 也被指定为0,设置为其他值也是无效的,这些内容都会被忽略掉。

    关于枚举序列化反序列化问题

    栗子一:下面通过一个栗子来验证一下:

    public enum Color /*implements Serializable*/ {//枚举本身就是实现了 Serializable 因此这里不需要去实现哦~
        RED, GREEN, BLUE;
        //枚举类的 serialVersionUID 会被 JVM 忽略
        private static final long serialVersionUID = 1L;
        public static void main(String[] args) {
    
            ObjectStreamClass desc = ObjectStreamClass.lookup(Color.RED.getClass());
            //枚举类的 serialVersionUID 都是 0
            //因此枚举类在增加新的枚举对象时并不会出现反序列化失败问题
            long serialVersionUID = desc.getSerialVersionUID();//0
            System.out.println(serialVersionUID);
        }
    }
    

    上面是用于验证枚举的 serialVersionUID 值的,从输出结果可以知道尽管 Color 类设置了 serialVersionUID = 1L,但是我们通过 ObjectStreamClass#getSerialVersionUID 得到的值仍然是0,有兴趣的读者可以验证一下。

    栗子二:验证枚举只序列化了 name 属性,而没有序列化 ordinal 属性

    从输出结果来看,序列化确实是没有序列化 orinal 属性,而只是序列化了 name 属性,而在反序列化时会根据 name 属性去调用 valueOf(name) 得到对应的枚举对象。

    public enum Color2 {
    
        //序列化前的排序:RED,GREEN, BLUE;
    //    RED,GREEN, BLUE;
        //序列化后的排序:GREEN, RED, BLUE;
        GREEN, RED, BLUE;
        private static final String PATH = "./color.bin";
    
        /**
         * 序列化前的结果
         * RED-0
         * GREEN-1
         * BLUE-2
         * ---------------------
         * 反序列化得到的输出结果
         * RED-1
         * GREEN-0
         * BLUE-2
         */
        public void printValue() {
            System.out.println(RED.name() + "-" + RED.ordinal());
            System.out.println(GREEN.name() + "-" + GREEN.ordinal());
            System.out.println(BLUE.name() + "-" + BLUE.ordinal());
        }
    
        public static void main(String[] args) throws Exception {
    
            serialEnum();
    
    //        deserialEnum();
        }
    
        private static void serialEnum() throws IOException {
            /**
             * 序列化 Color2.GREEN
             */
            Color2.GREEN.printValue();
            ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(PATH));
            oos.writeObject(Color2.GREEN);
            oos.flush();
            oos.close();
        }
    
        private static void deserialEnum() throws Exception {
            //反序列化
            ObjectInputStream ois = new ObjectInputStream(new FileInputStream(PATH));
            Color2 color2 = (Color2) ois.readObject();
            color2.printValue();
    
        }
    }
    

    类结构演化

    例如在序列化后反序列化前将类中增加或者删除某一个属性,对反序列化过程是不会报错的(前提是你将 serialVersionUID 值固定了),如果你是改变的属性的类型,那么肯定是会报错的。

    如果类型发生改变,那么在反序列化时会报如下错误:

    Exception in thread "main" java.io.InvalidClassException: com.example.seriable.Student; incompatible types for field age
        at java.io.ObjectStreamClass.matchFields(ObjectStreamClass.java:2299)
        at java.io.ObjectStreamClass.getReflector(ObjectStreamClass.java:2193)
        at java.io.ObjectStreamClass.initNonProxy(ObjectStreamClass.java:669)
        at java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:1630)
        at java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1521)
        at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:1781)
        at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1353)
        at java.io.ObjectInputStream.readObject(ObjectInputStream.java:373)
        at com.example.seriable.SerializableTest.deserialize(SerializableTest.java:83)
    

    单例的序列化和反射问题

    下面来看一个比较简单的单例实现:

    public class Singleton implements Serializable {
    
        private static final String PATH ="./singleton.bin" ;
        private volatile static  Singleton INSTANCE = null;
    
        private Singleton() {
        }
    
        public static Singleton getInstance() {
    
            if (INSTANCE == null) {
                synchronized (Singleton.class) {
                    if (INSTANCE == null) {
                        INSTANCE = new Singleton();
                    }
                }
            }
            return INSTANCE;
        }
    
    
    
        public static void main(String[] args) throws Exception{
            serialSington();
            deserialSington();
        }
            //序列化
        private static void serialSington() throws IOException {
            ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(PATH));
            oos.writeObject(Singleton.getInstance());
            oos.flush();
            oos.close();
        }
    
        private static void deserialSington() throws Exception {
            //反序列化
            ObjectInputStream ois = new ObjectInputStream(new FileInputStream(PATH));
            Singleton singleton = (Singleton) ois.readObject();
            System.out.println(singleton);
            System.out.println(Singleton.getInstance());
        }
    }
    

    上面的 main 函数的执行结果如下:

    很明显这两个对象是不一样的,违背了单例的原则。

    com.example.singleton.Singleton@58372a00
    com.example.singleton.Singleton@330bedb4
    

    解决方法呢?我们在 Singleton 类中加入一下方法,并返回 INSTANCE 该单例的对象

    private Object readResolve() {
        return INSTANCE;
    }
    

    上面的 main 函数的执行结果如下:

    com.example.singleton.Singleton@330bedb4
    com.example.singleton.Singleton@330bedb4
    

    这样通过 readResolve() 就解决了单列序列化问题了。

    接下来反射问题呢?

    通过一个静态变量 flag 来标识类对象是否已经被创建过,创建过对象之后,如果再次反射去创建那么将会抛出异常。但是呢?我也是可以反射去修改 flag 值呢,所以说单例在反射面前就有点尴尬了。

    public class Singleton2 {
    
        private volatile static Singleton2 INSTANCE = null;
        private static boolean flag = false;
    
        private Singleton2() {
            if (!flag) {
                flag = true;
            } else {
                throw new RuntimeException("单例不允许多次创建");
            }
        }
    
        public static Singleton2 getInstance() {
    
            if (INSTANCE == null) {
                synchronized (Singleton2.class) {
                    if (INSTANCE == null) {
                        INSTANCE = new Singleton2();
                    }
                }
            }
            return INSTANCE;
        }
    
        public static void main(String[] args) throws Exception {
            Singleton2 instance = Singleton2.getInstance();
            Class<Singleton2> singletonClazz = Singleton2.class;
            Constructor<Singleton2> constructor = singletonClazz.getDeclaredConstructor();
            constructor.setAccessible(true);
            Singleton2 singleton2 = constructor.newInstance();
            System.out.println(singleton2);
            System.out.println(instance);
        }
    }
    

    本文是笔者学习之后的总结,方便日后查看学习,有任何不对的地方请指正。

    记录于 2019年4月24号

    相关文章

      网友评论

        本文标题:序列化机制——Serializable工作原理

        本文链接:https://www.haomeiwen.com/subject/dyguoqtx.html