zoukankan      html  css  js  c++  java
  • Java对象的序列化和反序列化源码阅读

    前言

    序列化和反序列化看起来用的不多,但用起来就很关键,因为稍一不注意就会出现问题。序列化的应用场景在哪里?当然是数据存储和传输。比如缓存,需要将对象复刻到硬盘存储,即使断电也可以重新反序列化恢复。下面简单理解序列化的用法以及注意事项。

    如何序列化

    Java中想要序列化一个对象,必须实现Serializable接口。然后就可以持久化和反序列化了。下面是一个简单用法。

    项目测试代码:
    https://github.com/Ryan-Miao/someTest/blob/master/src/main/java/com/test/java/serial/TestSerialize.java

    我们给一个测试类:

    package com.test.java.serial;
    
    import lombok.Builder;
    import lombok.Data;
    
    import java.io.Serializable;
    
    /**
     * @author Ryan Miao
     */
    @Data
    @Builder
    public class Foo implements Serializable {
    
        private static final String LOGGER = "logger";
        public static final String PUB_STATIC_FINAL = "publicStaticFinal";
        public static String PUB_STATIC;
    
        public String fa;
        private String fb;
        transient public String ta;
        transient private String tb;
    }
    
    

    然后,测试序列化和反序列的数据是否丢失。

    
    public class TestSerialize {
    
        private static final String filename = "D:/test.txt";
    
        @Test
        public void testSer() throws IOException, ClassNotFoundException {
            final Foo foo = Foo.builder()
                    .fa("fa")
                    .fb("fb")
                    .ta("ta")
                    .tb("tb")
                    .build();
    
            Foo.PUB_STATIC  = "test";
    
            ObjectOutputStream os = new ObjectOutputStream(
                    new FileOutputStream(filename));
            os.writeObject(foo);
            os.flush();
            os.close();
    
        }
    
        @Test
        public void testRead() throws IOException, ClassNotFoundException {
            ObjectInputStream is = new ObjectInputStream(new FileInputStream(filename));
            Foo foo2 = (Foo) is.readObject();
            is.close();
    
            Assert.assertEquals("fa", foo2.getFa());
            Assert.assertEquals("fb", foo2.getFb());
            Assert.assertEquals(null, foo2.getTa());
            Assert.assertEquals(null, foo2.getTb());
    
            Assert.assertNull(foo2.PUB_STATIC);
        }
    }
    
    

    显然,transient修饰的字段不能被序列化,至于静态字段,这里不做测试,但要清楚。静态字段只和class类相关,和实例无关。而序列化是针对实例的,所以无所谓对比内容变化。那么,静态字段反序列化后数据是什么样子的呢?当然是类变量本身应该的样子。如果没有初始化,则是默认值, 本测试中的结果为null。

    为什么可以序列化

    我们只要实现了Serialiable就可以序列化,那么为什么呢?查看ObjectOutputStreamwriteObject方法。

    
    // remaining cases
    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() + "
    " + debugInfoStack.toString());
        } else {
            throw new NotSerializableException(cl.getName());
        }
    }
    

    显然,只针对String,Enum以及Serializable做了处理,因此想要序列化必须要实现这个接口。当然,String和Enum也实现了Serializable。

    如何自定义序列化,Java基础类库中的ArrayList等为什么用transient还能序列化

    简单的对象,对于不想序列化的字段,只要声明为transient就好。而有时候,我想对部分字段处理后序列化。比如ArrayList中存储数据的transient Object[] elementData;。我们知道ArrayList是可以序列化的,根源就在于自定义这里了。下面跟踪ObjectOutputStream源码,知道自定义的执行部分就可以验证了。

    入口: java.io.ObjectOutputStream#writeObject

    
    public final void writeObject(Object obj) throws IOException {
        if (enableOverride) {
            writeObjectOverride(obj);
            return;
        }
        try {
            writeObject0(obj, false);
        } catch (IOException ex) {
            if (depth == 0) {
                writeFatalException(ex);
            }
            throw ex;
        }
    }
    

    然后,核心方法

    
    private void writeObject0(Object obj, boolean unshared)
            throws IOException{
        boolean oldMode = bout.setBlockDataMode(false);depth++;
        try {
            //省略若干行
            for (;;) {
                // 省略若干行
                desc = ObjectStreamClass.lookup(cl, true);
                //省略若干行
            }
            //省略若干行
            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 {
                //....
            }
        } finally {
            depth--;
            bout.setBlockDataMode(oldMode);
        }
    }
    

    这里,显然可以看到真正的执行序列化代码是writeOrdinaryObject(obj, desc, unshared);。 但直接追踪进去发现里面有许多初始化的字段是在之前做的处理。因此,先卖个关子,看前面初始化的部分,只找到我们想要初始化的字段即可。

    进入desc = ObjectStreamClass.lookup(cl, true);

    
    static ObjectStreamClass lookup(Class<?> cl, boolean all) {
        //省略若干行
        if (entry == null) {
            try {
                entry = new ObjectStreamClass(cl);
            } catch (Throwable th) {
                entry = th;
            }
            //.....
        }
        //省略若干行
    }
    

    进入entry = new ObjectStreamClass(cl);这里就是真正的初始化地方,前面省略的代码是缓存处理,当然缓存使用的ConcurrentHashMap。

    
    private ObjectStreamClass(final Class<?> cl) {
        //省略无数行以及括号
        writeObjectMethod = getPrivateMethod(cl, "writeObject",
                                new Class<?>[] { ObjectOutputStream.class },
                                Void.TYPE);
        readObjectMethod = getPrivateMethod(cl, "readObject",
                                new Class<?>[] { ObjectInputStream.class },
                                Void.TYPE);
        //省略无数行
    

    没错,费了这么大劲就是为了找到这两个method。通过反射,获取到目标class的两个私有方法writeObject, readObject。这两个就是自定义方法所在。

    初始化完毕之后,我们再来继续序列化的代码. 回到刚才的核心方法,找到writeOrdinaryObject(obj, desc, unshared);, 进入,然后,继续找到writeSerialData(obj, desc);, 到这里就是真正执行序列化的代码了。

    
    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()) {
                //....
                try {
                    curContext = new SerialCallbackContext(obj, slotDesc);
                    bout.setBlockDataMode(true);
                    slotDesc.invokeWriteObject(obj, this);
                    bout.setBlockDataMode(false);
                    bout.writeByte(TC_ENDBLOCKDATA);
                } finally {
                    //...
                }
    
                curPut = oldPut;
            } else {
                defaultWriteFields(obj, slotDesc);
            }
        }
    }
    

    显然,判断writeObject这个method是否初始化了,如果有,则直接调用这个方法,没有则默认处理。到此,跟踪完毕,我想要自定义序列化只要重写writeObject, readObject这两个方法即可。

    下面看看ArrayList是怎么做的

    private void writeObject(java.io.ObjectOutputStream s)
        throws java.io.IOException{
        // Write out element count, and any hidden stuff
        int expectedModCount = modCount;
        s.defaultWriteObject();
    
        // Write out size as capacity for behavioural compatibility with clone()
        s.writeInt(size);
    
        // Write out all elements in the proper order.
        for (int i=0; i<size; i++) {
            s.writeObject(elementData[i]);
        }
    
        if (modCount != expectedModCount) {
            throw new ConcurrentModificationException();
        }
    }
    

    因为数组被设置不允许序列化,先默认序列化其他信息,然后单独处理数组里的内容,挨着写入元素。然后,对应读取方法也要改。

    private void readObject(java.io.ObjectInputStream s)
        throws java.io.IOException, ClassNotFoundException {
        elementData = EMPTY_ELEMENTDATA;
    
        // Read in size, and any hidden stuff
        s.defaultReadObject();
    
        // Read in capacity
        s.readInt(); // ignored
    
        if (size > 0) {
            // be like clone(), allocate array based upon size not capacity
            ensureCapacityInternal(size);
    
            Object[] a = elementData;
            // Read in all elements in the proper order.
            for (int i=0; i<size; i++) {
                a[i] = s.readObject();
            }
        }
    }
    

    为什么要这么做?因为数组元素有很多空余空间,对我们来说不需要序列化。通过这样自定义,把需要的元素序列化,可以节省空间。

    serialVersionUID为什么有的有,有的没有,什么时候用,意义是什么

    以下内容来自: https://www.cnblogs.com/ouym/p/6654798.html

    什么是serialVersionUID ?

    serialVersionUID表示:“串行化版本统一标识符”(serial version universal identifier),简称UID

    serialVersionUID必须定义成下面这种形式:static final long serialVersionUID = xxxL;

    serialVersionUID 用来表明类的不同版本间的兼容性。有两种生成方式: 一个是默认的1L;另一种是根据类名、接口名、成员方法及属性等来生成一个64位的哈希字段 。

    为什么要声明serialVersionUID

    java.io.ObjectOutputStream代表对象输出流,它的writeObject(Object obj)方法可对参数指定的obj对象进行序列化,把得到的字节序列写到一个目标输出流中。 java.io.ObjectInputStream代表对象输入流,它的readObject()方法从一个源输入流中读取字节序列,再把它们反序列化为一个对象,并将其返回。

    只有实现了Serializable或Externalizable接口的类的对象才能被序列化。

    Externalizable接口继承自Serializable接口,实现Externalizable接口的类完全由自身来控制序列化的行为,而仅实现Serializable接口的类可以采用默认的序列化方式 。 凡是实现Serializable接口的类都有一个表示序列化版本标识符的静态变量:private static final long serialVersionUID;

    类的serialVersionUID的默认值完全依赖于Java编译器的实现,对于同一个类,用不同的Java编译器编译,有可能会导致不同的serialVersionUID。显式地定义serialVersionUID有两种用途:

    1. 在某些场合,希望类的不同版本对序列化兼容,因此需要确保类的不同版本具有相同的serialVersionUID;在某些场合,不希望类的不同版本对序列化兼容,
      因此需要确保类的不同版本具有不同的serialVersionUID。
    2. 当你序列化了一个类实例后,希望更改一个字段或添加一个字段,不设置serialVersionUID,所做的任何更改都将导致无法反序化旧有实例,并在反序列化时抛出一个异常。
      如果你添加了serialVersionUID,在反序列旧有实例时,新添加或更改的字段值将设为初始化值(对象为null,基本类型为相应的初始默认值),字段被删除将不设置。

    注意事项

    1. 序列化时,只对对象的状态进行保存,而不管对象的方法;

    2. 当一个父类实现序列化,子类自动实现序列化,不需要显式实现Serializable接口;

    3. 当一个对象的实例变量引用其他对象,序列化该对象时也把引用对象进行序列化;

    4. 并非所有的对象都可以序列化,,至于为什么不可以,有很多原因了,比如:

      1. 安全方面的原因,比如一个对象拥有private,public等field,对于一个要传输的对象,比如写到文件,或者进行rmi传输等等,在序列化进行传输的过程中,这个对象的private等域是不受保护的。
      2. 资源分配方面的原因,比如socket,thread类,如果可以序列化,进行传输或者保存,也无法对他们进行重新的资源分 配,而且,也是没有必要这样实现。

    参考

  • 相关阅读:
    vb中5种打开文件夹浏览框的方法总结(转)
    js 日期
    正则表达式
    HTML中meta的作用
    Java操作ElasticSearch
    Elastic安装
    Elasticsearch基本操作
    ubootfs2410移植(转)
    内核驱动系列字符设备驱动流程
    linux 内核移植
  • 原文地址:https://www.cnblogs.com/woshimrf/p/java-serialize.html
Copyright © 2011-2022 走看看